Files
progcode/editorial/agent-rewrites/172.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
17 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": 172,
"slug": "editorial-2023-03-field-csrf-cors",
"title": "CORS, preflight и CSRF: как найти настоящую причину ошибки cookie API",
"excerpt": "Интерфейс теряет ответ, OPTIONS получает отказ, а mutation заканчивается 403. Разбираем границы CORS и CSRF, проверяем контракт по фактам и не снимаем защиту ради быстрого зелёного теста.",
"contentHtml": "<p>После переноса frontend на новый host интерфейс перестаёт читать ответ API. В консоли появляется CORS error. Другой запрос получает 403 от CSRF middleware. Иногда сервер уже выполнил mutation, а браузер только скрыл ответ. Ошибка стоит дорого: команда может открыть API для любого origin, отключить проверку токена или повторить действие пользователя, не понимая, был ли запрос принят.</p>\n<p>Тезис простой: CORS и CSRF проверяют разные свойства запроса. CORS ограничивает, какой origin может прочитать cross-origin response из браузера. CSRF защищает изменение состояния от запроса без доказательства намерения пользователя. Один заголовок CORS не заменяет CSRF-токен. CSRF-токен не чинит отсутствующий preflight contract. Сначала нужно восстановить request contract, потом исправлять ровно его нарушенную часть.</p>\n<h2>Начните с наблюдаемого симптома</h2>\n<p>Зафиксируйте одну операцию. Запишите origin страницы, URL API, method, content type, режим credentials и имена пользовательских заголовков. Добавьте status, видимые response headers и безопасный корреляционный идентификатор. Запись «браузер ругается на CORS» слишком коротка: она не показывает, был ли preflight, дошёл ли actual request до приложения и выполнился ли side effect.</p>\n<p>Не берите для проверки production cookie. В учебном или тестовом окружении создайте отдельную сессию, выберите тестовую запись и заранее опишите допустимый side effect. Если controlled browser check пока невозможен, так и напишите: контракт проверен статически, сеть и сессия не проверены. Недостающий evidence — это результат диагностики, а не повод объявлять защиту рабочей.</p>\n<h2>Механизм: три границы, которые нельзя склеивать</h2>\n<p><strong>CORS.</strong> Браузер сравнивает origin страницы с политикой ответа. Origin включает схему, host и port. Поэтому <code>https://app.example.test</code> и <code>https://app.example.test:8443</code> — разные значения. Для credentialed response сервер должен вернуть конкретный разрешённый origin и <code>Access-Control-Allow-Credentials: true</code>. Значение <code>*</code> не является заменой allow-list для запроса с credentials.</p>\n<p><strong>Preflight.</strong> Перед некоторыми cross-origin запросами браузер отправляет OPTIONS с вопросом о method и заголовках. Custom header вроде <code>X-CSRF-Token</code>, method <code>PATCH</code> и многие варианты JSON меняют request shape. Ответ OPTIONS должен разрешить только нужные method и header. Успешный OPTIONS не доказывает, что CSRF-токен валиден: он проверяет возможность выполнить запрос по CORS-контракту.</p>\n<p><strong>CSRF.</strong> Cookie может автоматически приложиться к запросу, созданному другим сайтом. Серверу нужно отдельное доказательство, что запрос сформировало разрешённое приложение. В token-based схеме сервер выдаёт непредсказуемый токен, а затем сравнивает его с токеном в сессии или с корректно связанным double-submit значением до mutation. Отсутствующий или неверный токен должен остановить side effect.</p>\n<pre><code>// Учебный пример контракта. Он не является готовым middleware.\nconst allowedOrigin = 'https://app.example.test';\n\nfunction corsHeaders(requestOrigin) {\n if (requestOrigin !== allowedOrigin) return {};\n\n return {\n 'Access-Control-Allow-Origin': allowedOrigin,\n 'Access-Control-Allow-Credentials': 'true',\n 'Vary': 'Origin',\n };\n}\n\nfunction preflightHeaders(requestMethod, requestHeaders) {\n const methods = ['POST'];\n const headers = ['Content-Type', 'X-CSRF-Token'];\n\n if (!methods.includes(requestMethod)) return null;\n if (requestHeaders.some((name) =&gt; !headers.includes(name))) return null;\n\n return {\n 'Access-Control-Allow-Methods': 'POST',\n 'Access-Control-Allow-Headers': 'Content-Type, X-CSRF-Token',\n };\n}\n\nfunction authorizeMutation(session, token) {\n if (!token || token !== session.csrfToken) {\n return { status: 403, reason: 'csrf-token-mismatch' };\n }\n return { status: 204 };\n}</code></pre>\n<p>Этот код показывает только идею: exact origin, узкий preflight и проверка токена до изменения состояния. Он не решает rotation, хранение сессии, кэширование HTML, logout, права на ресурс, обработку прокси и защиту от XSS. В production используйте contract и тесты выбранного framework. Не переносите учебный фрагмент в приложение без проверки его жизненного цикла.</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 отсутствует в allow-list</td><td>Сверить полный origin с <code>Access-Control-Allow-Origin</code></td><td>Добавить только нужный origin</td></tr><tr><td>Credentials response заблокирован</td><td>Wildcard или нет <code>Allow-Credentials</code></td><td>Проверить пару заголовков и режим credentials</td><td>Вернуть exact origin и явный credentials contract</td></tr><tr><td>OPTIONS получает отказ</td><td>Method или custom header не разрешён</td><td>Сопоставить request headers с ответом preflight</td><td>Разрешить один нужный method/header</td></tr><tr><td>POST form получил 403</td><td>CSRF-токен отсутствует или не совпал</td><td>Проверить server reason до mutation</td><td>Исправить выдачу и передачу токена</td></tr><tr><td>Mutation прошёл, UI увидел error</td><td>Server action и CORS visibility различаются</td><td>Сверить application log и browser evidence</td><td>Исправить CORS, не снимая CSRF</td></tr><tr><td>403 после token check</td><td>Нет business permission</td><td>Разделить CSRF reason и authorization reason</td><td>Исправить policy ресурса</td></tr></tbody></table>\n<h2>Иллюстрация маршрута</h2>\n<figure><img src=\"/assets/editorial/2023/csrf-cors-2023-field-diagnosis.svg\" alt=\"Маршрут диагностики CORS, preflight и CSRF\" /><figcaption>Схема разделяет origin policy, credentialed response, preflight и серверную проверку CSRF. Это учебная иллюстрация, а не trace реального запроса.</figcaption></figure>\n<p>Смотрите на рисунок как на карту evidence. Сначала фиксируйте вход запроса и его status. Затем определяйте, был ли OPTIONS и дошёл ли actual request до приложения. После этого отдельно проверяйте token и permission. Console message нельзя использовать как доказательство того, что сервер не видел запрос.</p>\n<h2>Разберите credentials и preflight отдельно</h2>\n<p>Если frontend действительно работает на другом origin и использует cookie, проверьте две пары. Первая пара — client credentials mode и <code>Access-Control-Allow-Credentials: true</code>. Вторая — точный source origin и <code>Access-Control-Allow-Origin</code>. Заголовки должны описывать согласованный контракт, а динамический ответ по origin должен учитывать кэширование, обычно через <code>Vary: Origin</code>.</p>\n<p>Затем выпишите фактический method и имена заголовков из client code. Не разрешайте «все методы и все headers» ради того, чтобы прекратить ошибку. Широкий ответ ухудшает review и превращает ошибку клиента в незаметно разрешённый путь. Если используется form-shaped POST, проверьте его отдельно: отсутствие preflight не означает отсутствие CSRF-риска.</p>\n<p>Если запрос требует preflight, actual request может не начаться после отказа OPTIONS. Для другого request shape сервер может принять HTTP-запрос, но браузер не даст JavaScript прочитать response. Поэтому нужны оба слоя: browser DevTools или HAR и proxy/application evidence с корреляционным идентификатором. Один слой не заменяет второй.</p>\n<h2>Маршрут проверки</h2>\n<ol><li><strong>Зафиксируйте симптом.</strong> Выберите одну failing операцию и назовите цену ошибки: потерянный response, отклонённый mutation или возможный side effect без видимого результата.</li><li><strong>Опишите request contract.</strong> Запишите source origin, target URL, method, content type, credentials и custom headers. Отдельно отметьте, что пока неизвестно.</li><li><strong>Проверьте CORS.</strong> Сопоставьте origin с ответом. Для credentials проверьте exact value, а не wildcard. Проверьте <code>Vary: Origin</code>, если ответ зависит от входного origin.</li><li><strong>Проверьте OPTIONS.</strong> Если request shape требует preflight, сравните фактические method и header names с узким allow-list. Не делайте вывод о CSRF по результату OPTIONS.</li><li><strong>Проверьте server-side proof.</strong> Убедитесь, что missing или mismatch token отклоняется до mutation. Причину CSRF не смешивайте с причиной отсутствия права.</li><li><strong>Исправьте одну причину.</strong> Добавьте exact origin, один method/header или недостающую передачу token. Не меняйте одновременно CORS на глобально permissive и CSRF на disabled.</li><li><strong>Повторите положительный и отрицательный путь.</strong> Разрешённый запрос должен получить ожидаемый response. Запрос без token, с неверным token и с чужим origin должен остаться запрещённым в соответствующем слое.</li><li><strong>Оставьте проверку.</strong> Закрепите route test для missing/mismatch token и проверяемую конфигурацию CORS. Для нового frontend origin нужен отдельный review, а не копия существующей строки.</li></ol>\n<h2>Отрицательный путь важнее зелёного запроса</h2>\n<p>Проверка только успешного запроса не доказывает защиту. Учебный тест должен явно показывать, что origin с другим port не получает credentialed response, неизвестный header не проходит preflight, а POST без token не меняет состояние. Это assertions над моделью контракта. Они не запускают gateway, браузер, framework middleware или настоящую session store.</p>\n<p>После PASS такого теста корректная формулировка звучит так: «проверены заданные правила контракта и отрицательные ветки». Нельзя писать «CORS и CSRF проверены в сети», если не было controlled browser/API evidence. Нельзя переносить в заметку production cookie, token values и идентификаторы реальных пользователей.</p>\n<h2>Ограничения и критерий готовности</h2>\n<p>Этот маршрут не заменяет XSS review, аудит cookie attributes, CSP, authorization test или penetration test. XSS на доверенном origin меняет картину: чужой скрипт может использовать доступные ему API и токены. CORS и CSRF не защищают от выполнения вредоносного JavaScript внутри собственного origin. Не существует универсального значения TTL токена, набора <code>SameSite</code> или списка trusted origins: решение зависит от framework, browser support, session model и threat model.</p>\n<p>Endpoint готов к review, когда видны четыре доказательства: точный разрешённый origin; корректный credentialed response и preflight contract, если они нужны; server-side rejection без CSRF proof до side effect; отдельная проверка business permission. Если есть только CORS header, работа не готова. Если есть только token test, frontend всё ещё может не прочитать response. Если есть только fixture PASS, нет доказательства интеграции. Эти слои дополняют друг друга и не заменяют друг друга.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://fetch.spec.whatwg.org/#http-cors-protocol\" target=\"_blank\" rel=\"noopener\">WHATWG Fetch Standard: HTTP CORS protocol</a> — нормативное описание CORS и credentialed requests.</li><li><a href=\"https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS\" target=\"_blank\" rel=\"noopener\">MDN: Cross-Origin Resource Sharing</a> — практическая справка по preflight, credentials и wildcard.</li><li><a href=\"https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html\" target=\"_blank\" rel=\"noopener\">OWASP: Cross-Site Request Forgery Prevention Cheat Sheet</a> — token patterns и требования к серверной проверке.</li></ul>"
}