Files
progcode/editorial/agent-rewrites/174.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
14 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": 174,
"slug": "editorial-2023-03-practice-csrf-cors",
"title": "Cookie API и виджет: как не перепутать CORS с CSRF",
"excerpt": "Разберите CORS и CSRF по отдельности: один механизм управляет доступом JavaScript к ответу, другой решает на сервере, можно ли принять изменение данных.",
"contentHtml": "<p>Виджет на <code>https://app.example.test</code> вызывает API на <code>https://api.example.test</code>. После релиза в DevTools появляется CORS error. Пользователь не видит профиль, а команда предлагает поставить <code>Access-Control-Allow-Origin: *</code>. Если API использует cookie, это не исправление. Браузер всё равно скроет credentialed response, а попытка отключить CSRF-проверку может открыть изменение данных с чужой страницы.</p>\n<p>Цена ошибки двойная. Рабочий интерфейс перестаёт получать данные. Одновременно сервер может начать принимать state-changing запрос только по cookie. Тогда злоумышленник не обязан читать ответ: ему достаточно заставить браузер жертвы отправить перевод, сменить адрес или удалить запись.</p>\n<p><strong>Тезис:</strong> CORS и CSRF отвечают на разные вопросы. CORS определяет, получит ли JavaScript cross-origin доступ к response. CSRF-защита проверяет на сервере, действительно ли запрос на изменение состояния пришёл из разрешённого сценария. Исправляйте эти контуры раздельно.</p>\n<h2>Механизм по шагам</h2>\n<p>Сначала браузер определяет origin. Это комбинация scheme, host и port. Для <code>https://app.example.test</code> и <code>https://api.example.test</code> host различается, поэтому запрос cross-origin. Путь и query в origin не входят. Общий registrable domain тоже не делает два приложения одним origin.</p>\n<p>Клиент может запросить credentials: например, передать cookie через <code>fetch(url, { credentials: 'include' })</code>. Это только намерение клиента. Сервер должен вернуть точный <code>Access-Control-Allow-Origin</code> для разрешённого origin и <code>Access-Control-Allow-Credentials: true</code>, если браузер должен открыть response JavaScript-коду. Wildcard <code>*</code> не совместим с credentialed CORS.</p>\n<p>CORS не является authorization. Успешная проверка CORS не означает, что пользователь вошёл, имеет право менять конкретный ресурс или передал CSRF-доказательство. Сервер должен выполнить аутентификацию и authorization независимо от CORS.</p>\n<p>CSRF появляется потому, что браузер может приложить cookie к cross-site запросу. Простая HTML-форма способна отправить POST с <code>application/x-www-form-urlencoded</code> без доступа к ответу и без preflight. Если endpoint меняет состояние только по cookie, такой запрос опасен.</p>\n<p>Для stateful API сервер обычно хранит CSRF-token в сессии и требует его в form field или custom header. Обработчик сравнивает token до mutation. Отсутствующий или неверный token даёт отказ. Origin или Referer check может добавить защиту, но не заменяет token, authorization и проверку бизнес-прав.</p>\n<h2>Конкретный пример</h2>\n<p>Ниже учебный пример политики. Он не открывает сеть, не создаёт cookie, не запускает браузер и не доказывает поведение конкретного production API. В нём показаны две независимые проверки: CORS для чтения ответа и CSRF для изменения состояния.</p>\n<pre><code>const corsOrigins = new Set(['https://app.example.test']);\nconst csrfOrigins = new Set([\n 'https://app.example.test',\n 'https://api.example.test',\n]);\n\nfunction checkRequest({ origin, credentials, method, csrfToken }) {\n const cors = corsOrigins.has(origin) &&\n (!credentials || origin !== '*');\n\n const safeMethod = new Set(['GET', 'HEAD', 'OPTIONS']).has(method);\n const csrf = safeMethod ||\n (csrfOrigins.has(origin) && csrfToken === 'token-from-session');\n\n return { cors: cors ? 'allow' : 'deny', csrf: csrf ? 'allow' : 'deny' };\n}\n\n// Учебные проверки, не production-конфигурация:\ncheckRequest({\n origin: 'https://app.example.test',\n credentials: true,\n method: 'POST',\n csrfToken: 'token-from-session',\n}); // { cors: 'allow', csrf: 'allow' }\n\ncheckRequest({\n origin: 'https://evil.example',\n credentials: true,\n method: 'POST',\n csrfToken: undefined,\n}); // { cors: 'deny', csrf: 'deny' }</code></pre>\n<p>Строка <code>token-from-session</code> здесь условна. В реальном приложении token должен быть непредсказуемым, связанным с сессией и сгенерированным безопасным источником случайности. Сравнение выполняет сервер до побочного эффекта. Не переносите этот код в middleware без проверки cookie policy, proxy и framework-контракта.</p>\n<p>Preflight полезен для диагностики формы запроса. Custom header вроде <code>X-CSRF-Token</code> или нестандартный content type часто вызывает OPTIONS-проверку. Но preflight не подтверждает token, сессию и право пользователя. Он проверяет, разрешает ли CORS-политика указанный origin, method и header. Поэтому нельзя считать preflight самостоятельной CSRF-защитой.</p>\n<figure><img src=\"/assets/editorial/2023/csrf-cors-2023-practice-contract.svg\" alt=\"Схема: приложение на app.example.test обращается к API на api.example.test; CORS и CSRF выполняют разные проверки\"><figcaption>CORS открывает JavaScript доступ к ответу только для точного origin. CSRF отдельно требует доказательство до изменения данных. Иллюстрация учебная: она не показывает реальный браузерный запуск или лог API.</figcaption></figure>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><thead><tr><th>Симптом</th><th>Причина</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>В консоли CORS error при cookie-запросе</td><td>Wildcard или отсутствует точный origin</td><td>Сверить <code>Origin</code>, <code>Access-Control-Allow-Origin</code>, credentials и <code>Vary: Origin</code></td><td>Оставить allow-list точных origin; не подставлять любое входное значение</td></tr><tr><td>OPTIONS проходит, POST меняет данные без token</td><td>Preflight ошибочно приняли за CSRF-защиту</td><td>Отправить form-shaped POST без custom header и проверить серверный ответ</td><td>Проверять CSRF-token до mutation для всех state-changing методов</td></tr><tr><td>403 после добавления token</td><td>Token не связан с текущей сессией или не доходит через proxy</td><td>Проверить источник token, cookie, заголовок, нормализацию и точку отказа</td><td>Исправить передачу и проверку; не отключать middleware целиком</td></tr><tr><td>Данные не читаются, но запись всё равно меняется</td><td>CORS скрывает response, но сервер принимает запрос по cookie</td><td>Смотреть server log и статус actual request отдельно от сообщения браузера</td><td>Добавить серверный CSRF-контроль и authorization</td></tr></tbody></table>\n<h2>Порядок действий</h2>\n<ol><li>Зафиксируйте страницу-источник, API URL, scheme, host и port. Не называйте два адреса одним «сайтом» без проверки origin.</li><li>Повторите запрос без изменений конфигурации. Сохраните method, content type, request headers, response status и серверный log.</li><li>Проверьте CORS отдельно. Для credentialed запроса нужен точный разрешённый origin и согласованный credentials response. Для публичного non-credentialed ресурса wildcard может быть уместен, но это другой контракт.</li><li>Перечислите все методы и endpoints, которые меняют состояние. Для каждого укажите источник CSRF-token и место проверки до mutation.</li><li>Проверьте отрицательный путь: POST из чужого origin, POST без token, неверный token и form-shaped POST без preflight должны получить отказ или не изменить состояние.</li><li>Проверьте authorization после CSRF. Валидный token не даёт пользователю право менять чужой ресурс.</li><li>Проверьте реальный browser flow с двумя контролируемыми origin. Учебная функция выше годится для проверки логики ветвлений, но не заменяет integration test.</li><li>Добавьте регрессионные проверки и наблюдение за отказами. В журнале не записывайте полный token и cookie.</li></ol>\n<h2>Ограничения</h2>\n<p>Эта схема предполагает cookie-based authentication и браузерный клиент. Она не описывает OAuth bearer token в заголовке, webhook, native app или server-to-server вызов. Для таких клиентов модель угроз и способ доказать полномочия будут другими.</p>\n<p>SameSite помогает ограничить отправку cookie, но не отменяет серверную проверку. Его результат зависит от атрибутов cookie, браузера, контекста навигации и окружения. Origin может отсутствовать или иметь значение <code>null</code>. Proxy может изменить набор видимых заголовков. Эти случаи нужно включить в отдельную политику, а не молча считать безопасными.</p>\n<p>XSS в доверенном origin может обойти многие CSRF-меры, потому что вредоносный код действует внутри разрешённого контекста. Поэтому исправление CSRF не заменяет защиту от XSS, управление cookie и контроль прав.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Сценарий готов, если команда может предъявить для одного state-changing endpoint четыре независимых доказательства: точный allow-list origin, ожидаемый CORS response, проверку CSRF-token до mutation и отказ при чужом origin или неверном token. В реальном browser test легитимный запрос читает response и меняет только разрешённый ресурс. Отрицательные запросы получают 403 или эквивалентный отказ, а состояние не меняется. Ни один из этих результатов нельзя заменять одним сообщением CORS в консоли.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS\" target=\"_blank\" rel=\"noopener noreferrer\">MDN: Cross-Origin Resource Sharing (CORS)</a> — правила wildcard, credentials и response headers.</li><li><a href=\"https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html\" target=\"_blank\" rel=\"noopener noreferrer\">OWASP: Cross-Site Request Forgery Prevention Cheat Sheet</a> — token-based защита и серверная проверка state-changing запросов.</li></ul>"
}