{ "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 не возвращают деньги и не отменяют изменение.
Цена такой подмены — ложное чувство защиты. CORS управляет чтением ответа из браузера. CSRF-защита управляет тем, может ли чужой сайт заставить браузер выполнить действие от имени пользователя. Эти механизмы стоят рядом, но решают разные задачи. Без явного пути атаки, контрольной точки и наблюдаемого evidence слово «проверено» слишком сильное.
\nТезис статьи простой: security review должен связывать asset, путь атаки, interruption и границу доказательства. Положительный результат подтверждает только эту связь. Он не разрешает deploy, не доказывает защиту production и не заменяет проверку входа, сессии, заголовков или поведения браузера.
\nПусть пользователь вошёл в bank.example. Браузер хранит cookie сессии и автоматически прикладывает её к запросу на этот origin. На странице evil.example размещена форма. Форма отправляет POST на банковский endpoint. Для простой формы браузер не обязан сначала выполнить CORS preflight. Сервер может получить cookie и изменить состояние.
<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 уменьшает поверхность, но его режим зависит от контекста браузера и схемы запроса. Без проверки на сервере нельзя считать один флаг достаточным.
Тот же endpoint может иметь корректный CORS и всё равно быть уязвимым к изменению состояния. И наоборот: endpoint может не разрешать чтение ответа чужому origin, но нуждаться в CSRF-токене для опасного действия. Сначала назовите действие. Потом проверьте, какой контроль его прерывает.
\nНачните с одного asset. Для примера это email пользователя. Путь атаки имеет порядок: чужая страница создаёт запрос, браузер добавляет cookie, endpoint принимает изменение, сервер сохраняет новый email. CORS находится на границе чтения ответа. Он не обязан останавливать первые три шага. CSRF-токен и серверная проверка origin находятся ближе к операции изменения.
\nУ каждого контроля должна быть одна фраза с глаголом. «CORS включён» ничего не говорит о действии. «Сервер отклоняет state-changing POST без валидного токена» описывает interruption. Такая запись проверяема: можно назвать вход, ответ и правило отказа. Если token проверяется только в JavaScript, контроль не стоит на серверной границе. Если endpoint разрешает запрос без cookie, нужно отдельно оценить анонимную операцию.
\nEvidence тоже имеет границу. Заголовок Access-Control-Allow-Origin показывает настройку чтения ответа. Он не показывает, что сервер отверг чужой POST. Ответ 403 на запрос без токена показывает одну отрицательную ветку. Он не доказывает, что все state-changing endpoints используют тот же middleware. Эти два наблюдения нельзя склеить в общий verdict.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Чужой origin видит CORS error | Браузер не отдаёт ему response body | Отправить отдельный state-changing POST и проверить запись | Добавить серверную CSRF-защиту, если действие использует cookie |
| POST проходит без token | Endpoint доверяет cookie без дополнительного доказательства намерения | Повторить запрос без token в изолированной учебной среде | Отклонять запрос до изменения состояния; сохранить ответ и correlation id |
| Token есть в форме, но не проверяется | Контроль остался на клиенте | Вызвать endpoint напрямую без выполнения UI | Перенести проверку на сервер и покрыть отрицательным тестом |
| Один endpoint защищён, другие нет | Проверка привязана к странице, а не к классу операции | Составить список state-changing routes и найти общий middleware | Назначить владельца непокрытых маршрутов; не выдавать общий verdict |
| После изменения появился 403 | Изменился контракт запроса или cookie policy | Сверить token, origin, cookie и права в позитивном сценарии | Исправить конкретный контракт; не ослаблять правило глобально |
Ниже псевдокод для учебного review. Он показывает порядок условий, но не является готовым middleware. Реальный фреймворк должен сам определить, как извлекать cookie, хранить token, сравнивать origin и формировать ответ.
\nfunction 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-симптома может не менять способность чужой формы отправить запрос.
Передача результата должна содержать четыре поля: threat id, control id, observed evidence и остаток. Например, threat — «чужая форма меняет email в сессии пользователя». Control — «сервер отклоняет POST без token и при несоответствующем origin». Evidence — «в учебном тесте запрос без token вернул 403 до вызова сохранения». Остаток — «не проверены другие маршруты и поведение production proxy».
\nТакой hand-off не означает, что система защищена. Он означает, что следующий человек видит, какую ветку повторить и где заканчивается наблюдение. Нельзя заменить остаток фразой «остальное стандартно». Нельзя перенести evidence с одного endpoint на весь API. Нельзя считать отсутствие ответа у атакующего доказательством отсутствия изменения в системе.
\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