8 lines
14 KiB
JSON
8 lines
14 KiB
JSON
{
|
||
"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>"
|
||
}
|