{ "index": 61, "slug": "editorial-2026-04-field-modern-web-security", "title": "Почему CORS не защищает POST: как проверить границы веб-безопасности", "excerpt": "Чужой сайт может отправить запрос с cookie даже после настройки CORS. Разбираем механизм, связываем угрозу с контролем и evidence, а затем получаем проверяемый stop или ограниченный hand-off.", "contentHtml": "

Симптом выглядит обнадёживающе: API отвечает на запросы только с нужным Origin, а в DevTools чужой сайт получает ошибку CORS. Команда помечает проблему закрытой. Но endpoint всё ещё принимает cross-site POST с cookie. Браузер может отправить запрос и не показать ответ атакующему. Если запрос меняет адрес доставки, пароль или лимит, ошибки CORS не возвращают деньги и не отменяют изменение.

\n

Цена такой подмены — ложное чувство защиты. CORS управляет чтением ответа из браузера. CSRF-защита управляет тем, может ли чужой сайт заставить браузер выполнить действие от имени пользователя. Эти механизмы стоят рядом, но решают разные задачи. Без явного пути атаки, контрольной точки и наблюдаемого evidence слово «проверено» слишком сильное.

\n

Тезис статьи простой: security review должен связывать asset, путь атаки, interruption и границу доказательства. Положительный результат подтверждает только эту связь. Он не разрешает deploy, не доказывает защиту production и не заменяет проверку входа, сессии, заголовков или поведения браузера.

\n

Один запрос показывает разницу между CORS и CSRF

\n

Пусть пользователь вошёл в bank.example. Браузер хранит cookie сессии и автоматически прикладывает её к запросу на этот origin. На странице evil.example размещена форма. Форма отправляет POST на банковский endpoint. Для простой формы браузер не обязан сначала выполнить CORS preflight. Сервер может получить cookie и изменить состояние.

\n
<form action=\"https://bank.example/profile/email\" method=\"POST\">\n  <input name=\"email\" value=\"attacker@example.net\">\n</form>\n<script>document.forms[0].submit()</script>\n\n// Учебный пример. Он не отправляется и не доказывает поведение\n// конкретного браузера или endpoint.
\n

Если сервер принимает такой POST только по cookie, запрос остаётся опасным. Проверка Origin или Referer может добавить условие. Synchronizer token или signed double-submit token связывает действие с формой приложения. Cookie с подходящим SameSite уменьшает поверхность, но его режим зависит от контекста браузера и схемы запроса. Без проверки на сервере нельзя считать один флаг достаточным.

\n

Тот же endpoint может иметь корректный CORS и всё равно быть уязвимым к изменению состояния. И наоборот: endpoint может не разрешать чтение ответа чужому origin, но нуждаться в CSRF-токене для опасного действия. Сначала назовите действие. Потом проверьте, какой контроль его прерывает.

\n

Механизм проверки: путь, контроль, evidence

\n

Начните с одного asset. Для примера это email пользователя. Путь атаки имеет порядок: чужая страница создаёт запрос, браузер добавляет cookie, endpoint принимает изменение, сервер сохраняет новый email. CORS находится на границе чтения ответа. Он не обязан останавливать первые три шага. CSRF-токен и серверная проверка origin находятся ближе к операции изменения.

\n

У каждого контроля должна быть одна фраза с глаголом. «CORS включён» ничего не говорит о действии. «Сервер отклоняет state-changing POST без валидного токена» описывает interruption. Такая запись проверяема: можно назвать вход, ответ и правило отказа. Если token проверяется только в JavaScript, контроль не стоит на серверной границе. Если endpoint разрешает запрос без cookie, нужно отдельно оценить анонимную операцию.

\n
\"Цикл
Граница hand-off должна быть видна. Учебный цикл передаёт только названный scope проверки и не превращается в решение о выпуске.
\n

Evidence тоже имеет границу. Заголовок Access-Control-Allow-Origin показывает настройку чтения ответа. Он не показывает, что сервер отверг чужой POST. Ответ 403 на запрос без токена показывает одну отрицательную ветку. Он не доказывает, что все state-changing endpoints используют тот же middleware. Эти два наблюдения нельзя склеить в общий verdict.

\n
От симптома к проверяемому действию
СимптомПричинаПроверкаДействие
Чужой origin видит CORS errorБраузер не отдаёт ему response bodyОтправить отдельный state-changing POST и проверить записьДобавить серверную CSRF-защиту, если действие использует cookie
POST проходит без tokenEndpoint доверяет cookie без дополнительного доказательства намеренияПовторить запрос без token в изолированной учебной средеОтклонять запрос до изменения состояния; сохранить ответ и correlation id
Token есть в форме, но не проверяетсяКонтроль остался на клиентеВызвать endpoint напрямую без выполнения UIПеренести проверку на сервер и покрыть отрицательным тестом
Один endpoint защищён, другие нетПроверка привязана к странице, а не к классу операцииСоставить список state-changing routes и найти общий middlewareНазначить владельца непокрытых маршрутов; не выдавать общий verdict
После изменения появился 403Изменился контракт запроса или cookie policyСверить token, origin, cookie и права в позитивном сценарииИсправить конкретный контракт; не ослаблять правило глобально
\n

Пример серверной границы

\n

Ниже псевдокод для учебного review. Он показывает порядок условий, но не является готовым middleware. Реальный фреймворк должен сам определить, как извлекать cookie, хранить token, сравнивать origin и формировать ответ.

\n
function updateEmail(request) {\n  if (request.method !== 'POST') return allowMethod();\n  if (!sameOrigin(request.headers.origin)) return reject(403);\n  if (!validCsrfToken(request.cookie, request.body.csrf)) {\n    return reject(403);\n  }\n  if (!validEmail(request.body.email)) return reject(400);\n  return saveEmail(request.session.userId, request.body.email);\n}\n\n// Учебный пример: не содержит production storage, logging\n// или конкретную реализацию token.
\n

Важен не синтаксис, а место проверки. Запрос отклоняется до записи. Позитивная ветка требует действующую сессию и корректный token. Негативная ветка проверяет запрос без token, с чужим origin и с повторно использованным token. Если тест вызывает только функцию в памяти, он подтверждает порядок условий в примере. Он не подтверждает маршрутизацию, cookie flags, proxy и реальную базу.

\n

Для CORS правило другое. Разрешайте конкретные origins, не отражайте произвольный заголовок Origin, не сочетайте wildcard с credentialed requests и проверяйте, нужен ли endpoint вообще для cross-origin чтения. Но даже строгий allowlist не заменяет CSRF-защиту. Это отрицательный путь статьи: исправление видимого CORS-симптома может не менять способность чужой формы отправить запрос.

\n

Как передавать результат без ложного допуска

\n

Передача результата должна содержать четыре поля: threat id, control id, observed evidence и остаток. Например, threat — «чужая форма меняет email в сессии пользователя». Control — «сервер отклоняет POST без token и при несоответствующем origin». Evidence — «в учебном тесте запрос без token вернул 403 до вызова сохранения». Остаток — «не проверены другие маршруты и поведение production proxy».

\n

Такой hand-off не означает, что система защищена. Он означает, что следующий человек видит, какую ветку повторить и где заканчивается наблюдение. Нельзя заменить остаток фразой «остальное стандартно». Нельзя перенести evidence с одного endpoint на весь API. Нельзя считать отсутствие ответа у атакующего доказательством отсутствия изменения в системе.

\n

Порядок действий

\n
  1. Назовите asset и действие. Запишите, что может изменить чужой запрос: email, пароль, заказ или иной объект.
  2. Нарисуйте путь. Укажите страницу-источник, cookie, endpoint, проверку и запись. Не объединяйте чтение ответа с изменением состояния.
  3. Разделите controls. Отдельно опишите CORS, CSRF token, origin check, SameSite и авторизацию. Для каждого назовите interruption.
  4. Проверьте отрицательный запрос. В учебной или специально разрешённой среде уберите token, измените origin и убедитесь, что запись не произошла.
  5. Проверьте позитивный запрос. С действующей сессией и корректным token операция должна пройти. Сохраните только наблюдаемые поля.
  6. Сверьте покрытие. Найдите все state-changing маршруты и убедитесь, что правило применяет общий серверный слой, а не одну форму.
  7. Запишите остаток. Назовите непроверенные proxy, браузеры, cookie-режимы, фоновые операции и endpoints.
  8. Передайте ограниченный результат. Если связка неполна, верните stop с точным следующим вопросом. Не превращайте учебный output в release approval.
\n

Ограничения и критерий готовности

\n

Эта проверка не измеряет вероятность атаки, размер ущерба, покрытие всех маршрутов, устойчивость к обходу proxy или состояние системы после deploy. Пример не обращается к реальному API, не запускает браузер и не использует production cookie. Официальные стандарты помогают выбрать вопросы, но не подтверждают конкретную конфигурацию. CSP, например, полезна как дополнительная граница для content injection, но не отменяет безопасную обработку данных. ASVS задаёт требования для верификации web-контролей, а не автоматический verdict. NIST описывает наборы техник проверки, а не единый сертификат.

\n

Критерий готовности должен быть узким и воспроизводимым: для каждого state-changing endpoint существует один записанный attack path, серверная проверка стоит до изменения состояния, позитивный запрос проходит с корректным token, отрицательный запрос не создаёт запись, а evidence содержит маршрут, вход, статус и границу применимости. Непокрытый маршрут остаётся stop. Если нельзя показать, какой контроль прервал путь, review не завершён.

\n

Если после проверки требуется изменить код или конфигурацию, это отдельная уполномоченная работа. Hand-off только указывает следующий шаг. Он не включает заголовки, не меняет cookie policy и не разрешает выпуск. Такое ограничение делает вывод уже, но честнее: команда знает, что проверила, чего не проверила и какую работу нельзя считать выполненной.

\n

Проверяемые источники

" }