Запросы с cookies и CSRF
Браузер может отправить подходящую cookie с запросом, который инициировал посторонний контекст. Сервер видит действующую сессию, но из одного этого факта не знает, хотел ли пользователь выполнить изменение. CSRF-защита рассматривает именно такую угрозу: автоматическое основание доступа сопровождает действие, не подтверждённое доверенным интерфейсом.
В этом уроке добавим две проверки для cookie-аутентифицированных изменений каталога: точный Origin и токен, связанный с текущей сессией. Результат — пригодная cookie не позволяет выполнить PUT без дополнительного контекста намерения. Ничего не отправляется по сети; значения и обмены составлены для объяснения.
Что cookie подтверждает и чего не подтверждает
sid-alice связывается с u17, ролью editor и областью frontend. Это помогает установить личность. Однако если браузер автоматически послал её при действии недоверенной страницы, личность сама по себе не доказывает разрешённый источник операции. SameSite ограничивает часть таких контекстов, но соседний поддомен может находиться в том же site.
Например, недоверенный https://promo.catalog.example имеет другой origin, хотя относится к общей области site. Его нельзя считать редактором только из-за имени. Политика SameSite не заменяет точное разрешение catalog и editor. Отсутствие CORS также не доказывает отсутствие отправки простого POST.
Поэтому GET нашего публичного курса не имеет побочного редактирования. Даже если его вызвали из чужого контекста, действие остаётся чтением. Изменения оформлены отдельными методами и должны проходить дополнительные серверные границы. Это фундаментальная часть модели, которую невозможно исправить одним токеном, оставив удаление за обычной ссылкой.
Токен принадлежит сессии
Доверенный интерфейс получает CSRF-токен через защищённый GET /api/csrf. Сервер возвращает значение, связанное с текущей сессией, и не сохраняет ответ в HTTP-кеше:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: private, no-store
{"token":"csrf-alice"}
Это не новый session id и не пароль. Без действующей сессии сам токен не создаёт u17. Настоящее значение должно генерироваться подходящим случайным механизмом и проверяться реализацией, а не быть предсказуемой строкой имени пользователя. В статье используется лишь условное обозначение.
Токен не кладут в URL, который легко попадает в историю, копирование и журналы. Доверенный код хранит его в своём состоянии и предъявляет отдельным полем X-CSRF-Token. HttpOnly сессионная cookie остаётся недоступной обычному JavaScript, а этот токен намеренно доступен доверенному интерфейсу для операции.
Принятое изменение
Покажем смысловой PUT Alice с обоими основаниями. Он также содержит прежнее условие версии, поскольку CSRF и защита от потери обновления решают разные задачи:
PUT /api/courses/c001 HTTP/1.1
Host: catalog.example
Cookie: __Host-catalog_sid=sid-alice
Origin: https://catalog.example
X-CSRF-Token: csrf-alice
Content-Type: application/json
Accept: application/json, application/problem+json
If-Match: "c001-json-r1"
{"title":"JavaScript для веба","topic":"frontend","lessons":40}
Сервис распознаёт сессию, проверяет её срок и права на c001, сопоставляет Origin с доверенным перечнем и токен с этой сессией. Затем допустимый вход применяется при атомарном совпадении версии. Если все границы пройдены, получаем тот же предметный успех PUT, что в предыдущей части.
Токен сессии другого пользователя не подходит, даже если обе сессии принадлежат редакторам. Он связан с конкретным основанием, а не только ролью. После ротации сессии клиент получает актуальный токен; старое значение не следует бессрочно принимать без заявленного жизненного цикла.
Отказ сохраняет курс
Изменим только X-CSRF-Token на неподходящее значение. В нашем договоре ожидается 403, и никакие поля курса не сохраняются. Аналогично отсутствующее поле, чужой Origin, null или отсутствующий Origin для cookie-записи дают отказ.
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Cache-Control: no-store
{"type":"https://catalog.example/problems/request-context-forbidden","title":"Контекст изменения не подтверждён","status":403}
Ответ не раскрывает ожидаемый секретный токен. Доверенный интерфейс может получить свежий через свой защищённый процесс, но не должен автоматически обходить отказ отключением проверки. Сервер также не делает исключение для запроса только потому, что в нём есть правильный If-Match: валидатор версии ничего не говорит о происхождении действия.
Проверка Origin использует точные origin catalog и editor с HTTPS. Подстрока, окончания сомнительного имени или произвольное отражение поля не подходят. Missing-Origin политика здесь намеренно строгая для cookie-записей. Программный клиент использует отдельный явно предъявленный Bearer без сессионной cookie.
Почему CORS недостаточно
CORS разрешает браузерному коду доступ к ответу другого origin и участвует в предварительном разрешении определённых обращений. Он не является универсальным доказательством, что любой запрещённый запрос не ушёл. В частности, простое обращение может достичь сервера до отказа чтения ответа.
Наш CSRF-договор проверяется непосредственно перед изменением независимо от CORS. Origin editor получает разрешение чтения и всё равно предъявляет токен. Origin promo не получает ни нужное право контекста изменения, ни разрешение на чтение личного токена. Так две политики поддерживают разные границы, не подменяя друг друга.
Access-Control-Allow-Credentials true не означает «любая cookie разрешает любую операцию». Он относится к доступу браузера к credentialed-ответу для подходящего origin. Роль Alice и допустимость c001 проверяются отдельно. Опасно расширить CORS до отражения всех origin, сохранив endpoint выдачи личного токена: это может разрушить предположение о доверенном чтении.
Bearer использует другую модель угроз
Программный клиент с Authorization Bearer и без автоматической session cookie предъявляет токен явно. Для такого режима наш API не требует описанный ambient-cookie CSRF-токен. Это не заявление, что Bearer решает все риски: похищение, XSS, неверный срок и область остаются задачами защиты credentials.
Браузерный Bearer-клиент исключает сессионную cookie через credentials omit, чтобы не смешивать режимы. Если вместе присутствуют оба основания, действует 400 ambiguous-credentials. Нельзя надеяться, что сервер случайно выберет более удобную схему и пропустит соответствующие проверки.
XSS в доверенном origin может выполнить действия от имени интерфейса и прочитать доступный ему CSRF-токен. Поэтому защита вывода, политика ресурсов и управление исполнением кода необходимы отдельно. Токен защищает выбранную угрозу межконтекстного действия, а не делает произвольный вредный скрипт безопасным. Модель CSRF и способы защиты OWASP.
Вход и выход тоже имеют контекст
Logout изменяет сессию, поэтому использует POST и тот же CSRF-договор. Удаление cookie без серверного отзыва не заменяет действие. При повторе после уже выполненного выхода credentials могут стать непригодны; клиент должен понимать такой исход, не обещая, что первый logout отказал.
Сам вход также способен иметь CSRF-риски и потребует отдельной защиты состояния до аутентификации при реализации. В этой серии нет готового login handler, поэтому условный успешный ответ Set-Cookie не доказывает правильность процесса проверки пароля. Границу следует сохранить явно.
Практический клиент также должен различать замену сессии и исправление данных курса. После нового входа прежний CSRF-токен уже не обязан подходить: он относился к старому sid. Клиент получает новый token через защищённое чтение, а незавершённую предметную операцию восстанавливает отдельно. Для повторяемого создания он сохраняет идентичность намерения, если разрешает прежний неопределённый исход. Для PUT он также сверяет текущую версию, а не считает новую сессию основанием перезаписать старую форму.
Теперь сравните правильную cookie с неверным токеном, правильный токен без сессии и полный доверенный запрос с устаревшим ETag. Каждый вариант останавливается на своей причине и сохраняет данные до принятия. Следующий урок применит эти основания к загрузке файла, где к политике доступа добавятся размер, содержимое и жизненный цикл временной записи.