Files
progcode/editorial/agent-rewrites/173.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
16 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 173,
"slug": "editorial-2023-03-mechanism-csrf-cors",
"title": "CSRF и CORS: как разделить доступ к ответу и право изменить данные",
"excerpt": "CORS решает, может ли чужой origin прочитать ответ. CSRF проверяет, разрешено ли cookie-аутентифицированному запросу менять состояние. Разбираем origin, credentials, preflight и отрицательные проверки на одном endpoint.",
"contentHtml": "<p>После переноса frontend на новый домен приложение начинает показывать <code>CORS error</code>. Одни запросы не видны JavaScript, другие получают 403, а часть операций, судя по логам, всё же доходит до API. Ошибка провоцирует опасную правку: разрешить <code>*</code>, принять любой origin или отключить CSRF middleware. Цена — не только сломанный интерфейс. Сервер может открыть ответ лишнему сайту или принять изменение от страницы, которая не выражала доверенный intent.</p>\n<p>Главный тезис простой: CORS и CSRF работают на разных границах. CORS ограничивает доступ браузерного кода к cross-origin response. CSRF защищает state-changing запрос, который использует учетные данные пользователя, например cookie сессии. Preflight проверяет форму запроса. Он не подтверждает token, пользователя или право на объект.</p>\n<h2>Сначала зафиксируйте наблюдаемый симптом</h2>\n<p>Не начинайте с заголовка, которого не хватает. Возьмите одну операцию и запишите пять значений: полный origin страницы, URL API, method, content type и имена request headers. Добавьте статус ответа и те response headers, которые видны в DevTools или на proxy boundary. Сообщение в консоли полезно как сигнал, но не объясняет, был ли отправлен actual request, скрыл ли браузер ответ или сервер отклонил mutation.</p>\n<p>Например, страница находится на <code>https://app.example.test</code>, а API — на <code>https://api.example.test</code>. Клиент вызывает <code>fetch()</code> с <code>credentials: 'include'</code> и отправляет <code>X-CSRF-Token</code>. Если OPTIONS не разрешает method или header, actual request может не начаться. Если preflight прошел, это ещё не значит, что token совпал с сессией. Если token совпал, пользователь всё ещё может не иметь права на конкретную запись.</p>\n<h2>Механизм: три независимых слоя</h2>\n<p><strong>Origin.</strong> Браузер сравнивает tuple из scheme, host и port. <code>https://app.example.test</code> и <code>http://app.example.test</code> имеют разные origins. Порт <code>8443</code> тоже меняет tuple. Path, query и fragment в origin не входят. Поэтому проверка вида <code>host.endsWith('example.test')</code> слишком широка: она превращает любой поддомен в доверенный источник.</p>\n<p>Origin — это техническая граница браузера, а не готовая модель бизнес-доверия. Даже точный <code>https://admin.example.test</code> не должен автоматически получать права user API. Его добавляют в allow-list только для конкретной причины, endpoint и набора данных.</p>\n<p><strong>CORS.</strong> Сервер сообщает браузеру, какому source origin можно отдать response браузерному коду. Для credentialed response нужен точный <code>Access-Control-Allow-Origin</code> и <code>Access-Control-Allow-Credentials: true</code>. Значение <code>*</code> нельзя совмещать с запросом, который использует credentials. Это правило отвечает за видимость представления ответа. Оно не авторизует mutation и не проверяет CSRF token.</p>\n<p><strong>CSRF.</strong> Cookie отправляется браузером автоматически по своим правилам. Сам факт наличия cookie не доказывает, что пользователь намеренно вызвал действие из доверенного интерфейса. Сервер должен проверить token, строгую origin policy или другой подходящий proof до побочного эффекта. После этого он отдельно проверяет authorization: имеет ли actor право выполнить действие над данным объектом.</p>\n<p><strong>Preflight.</strong> Браузер отправляет OPTIONS, когда форма cross-origin запроса выходит за CORS safelist. PATCH, JSON content type и custom header часто приводят к preflight. Успешный OPTIONS разрешает форму следующего запроса для указанного origin. Он не сравнивает CSRF token с сессией и не проверяет бизнес-права. Обычный form-shaped POST может не иметь preflight, хотя меняет состояние. Поэтому CSRF нельзя строить на предположении, что опасный запрос обязательно виден как OPTIONS.</p>\n<figure><img src=\"/assets/editorial/2023/csrf-cors-2023-mechanism-layers.svg\" alt=\"Три слоя cross-origin запроса: origin, CORS и CSRF\" /><figcaption>Схема помогает разделить tuple origin, доступ к response и серверную проверку state-changing запроса. Это учебная иллюстрация, а не трасса браузера и не доказательство доставки cookie.</figcaption></figure>\n<h2>Конкретный пример</h2>\n<p>Рассмотрим cookie-аутентифицированный endpoint <code>PATCH /profile</code>. Клиент живет на разрешенном origin и передает token в custom header. Учебный сервер сначала проверяет origin и token, затем право пользователя. Код показывает порядок условий, но не заменяет middleware, браузерный тест и проверку cookie attributes.</p>\n<pre><code>const trustedOrigins = new Set([\n 'https://app.example.test',\n]);\n\nfunction updateProfile(request, session) {\n const origin = request.headers.get('Origin');\n const token = request.headers.get('X-CSRF-Token');\n\n if (!trustedOrigins.has(origin)) {\n return deny(403, 'untrusted-origin');\n }\n if (!token || !constantTimeEqual(token, session.csrfToken)) {\n return deny(403, 'csrf-failed');\n }\n if (!session.user.can('profile:update')) {\n return deny(403, 'not-authorized');\n }\n\n return applyProfileChange(request.body);\n}</code></pre>\n<p>В этом примере <code>constantTimeEqual</code>, выдача token, rotation, logout и обработка ошибок должны существовать в реальном компоненте безопасности. Фрагмент намеренно учебный. Он показывает, что CORS response и CSRF proof находятся рядом, но не являются одной проверкой. Он также сохраняет отрицательный путь: неверный origin, отсутствующий token и отсутствие permission не должны доходить до <code>applyProfileChange</code>.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><thead><tr><th>Симптом</th><th>Причина</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>JavaScript не читает response</td><td>Origin не разрешен или заголовок не совпал</td><td>Сверить полный scheme, host и port с exact <code>Access-Control-Allow-Origin</code></td><td>Добавить только нужный origin и проверить cache policy</td></tr><tr><td>Credentials response заблокирован</td><td>Использован wildcard или отсутствует credentials header</td><td>Сопоставить <code>credentials: 'include'</code> с двумя response headers</td><td>Вернуть exact origin и <code>Access-Control-Allow-Credentials: true</code></td></tr><tr><td>OPTIONS получает 403</td><td>Не разрешены method или custom header</td><td>Сверить <code>Access-Control-Request-Method</code> и <code>Access-Control-Request-Headers</code></td><td>Разрешить узкий фактический набор, не все методы и headers</td></tr><tr><td>POST form получает 403</td><td>CSRF proof отсутствует или не совпал</td><td>Посмотреть server reason до mutation и проверить token flow</td><td>Исправить выдачу и передачу token; CSRF не отключать</td></tr><tr><td>UI видит CORS error, а запись изменилась</td><td>Сервер выполнил action, но браузер скрыл response</td><td>Сопоставить server log, status и browser network evidence</td><td>Исправить CORS visibility и отдельно сохранить CSRF/authorization</td></tr><tr><td>Token принят, но ответ 403</td><td>Не пройдена бизнес-авторизация</td><td>Разделить причины CSRF и permission в server log</td><td>Исправить policy ресурса, а не расширять CORS</td></tr></tbody></table>\n<h2>Порядок проверки одного endpoint</h2>\n<ol><li>Зафиксируйте failing operation и цену ошибки: данные не читаются, mutation отклоняется или action мог выполниться без видимого response.</li><li>Составьте точный request contract: source origin, target URL, method, content type, credentials mode и header names. Не заменяйте эти значения фразой «запрос с фронта».</li><li>Разделите этапы. Отдельно проверьте exact CORS response, отдельно OPTIONS, отдельно server-side CSRF proof и отдельно authorization.</li><li>Добавьте отрицательные проверки: другой scheme, другой port, незнакомый subdomain, отсутствующий token и token от другой сессии. Для каждого варианта ожидайте отказ до side effect.</li><li>Проверьте реальный browser flow в тестовой среде с безопасной test session. Сопоставьте DevTools или HAR с proxy/application status. Не переносите production cookie, token и пользовательские данные в фикстуру.</li><li>После исправления повторите исходный happy path и тот же отрицательный path. Успешный CORS response не отменяет CSRF assertion, а успешный token test не доказывает, что frontend прочитает ответ.</li><li>Закрепите узкое правило тестом и конфигурацией с понятным owner. Новый frontend origin должен проходить отдельный security review, а не появляться копированием существующей строки.</li></ol>\n<h2>Ограничения</h2>\n<p>Атрибуты cookie, SameSite policy, third-party cookie restrictions, redirects, proxy cache и режим приватности браузера влияют на фактическую доставку credentials. Поэтому нельзя заключить из одного response header, что cookie была отправлена. Нельзя и заключить из отсутствия OPTIONS, что запрос безопасен: form submission и некоторые safelisted shapes способны менять состояние.</p>\n<p>CSRF не защищает от XSS на уже доверенном origin. Скрипт, который получил выполнение в приложении, может использовать доступные ему API и token. CSRF также не заменяет authorization, rate limiting, audit log, CSP или контроль webhook и service-to-service клиентов. Для каждого caller нужен явный authentication contract.</p>\n<p>Учебный код ограничен. Он не реализует Fetch, не моделирует браузер, не проверяет все byte-level ограничения заголовков, не учитывает CORS cache и не подтверждает конкретную версию framework. Реальный результат появляется только из browser/integration проверки в контролируемой среде и server evidence.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Endpoint готов к изменению, если reviewer может назвать разрешенный source origin, увидеть exact credentialed CORS contract, воспроизвести нужный preflight или доказать его отсутствие, получить отказ без CSRF proof и отдельно подтвердить permission check. В тестовой среде server log показывает, что отрицательные ветки не вызвали side effect. Если есть только CORS header, работа не готова. Если есть только unit test token, не доказана интеграция с браузером. Если есть только ручной happy path, не защищен отрицательный путь.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://fetch.spec.whatwg.org/\" target=\"_blank\" rel=\"noopener noreferrer\">WHATWG Fetch Standard</a> — модель credentials mode, CORS protocol и preflight.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc6454\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 6454: The Web Origin Concept</a> — origin из scheme, host и port и заголовок <code>Origin</code>.</li></ul>"
}