Симптом в этом разборе — release checklist говорит «CSRF и headers включены», но на вопрос «какой именно запрос мы проверили?» ответа нет. Цена не абстрактная: при первой правке маршрута token может перестать доходить до controller, permission окажется в шаблоне, а ответ на ошибке потеряет policy. Тогда документация выглядит лучше реальности, а команда узнаёт о разрыве только после чужого сообщения или случайного инцидента.
Ниже не отчёт о живом приложении и не разрешённый security test. Это synthetic trace для одной формы POST /admin/profile на admin.example.test. Мы не посылаем пакеты, не запускаем browser и не перебираем endpoints. Вместо этого revision-модуль создаёт четыре учебных request object в памяти: ожидаемый, foreign origin, пустой token и недостаточное permission. Цель — увидеть, что каждый заявленный control даёт свой наблюдаемый outcome, а не доказать отсутствие всех уязвимостей.
Собираем evidence до обсуждения настроек
Для небольшого baseline нужны четыре вида evidence. Первый — карта актива: чему именно может навредить маршрут. Второй — request contract с ожидаемыми method, path, origin, token и permission. Третий — response contract с cookie attributes и CSP policy. Четвёртый — отрицательные решения, которые не дают изменению состояния пройти. Если оставить только конфигурацию, нельзя отличить «поле есть в файле» от «application gate действительно сработал».
В этом месте полезно держать историческую рамку. OWASP Top 10:2017 помогает назвать риск, а ASVS 4.0.2 от 28 октября 2020 года — превратить часть разговора в проверяемые требования. Но ни один документ не знает наш учебный route. Поэтому evidence начинается с конкретного актива, а не с claim о соответствии. Условие «мы увидели 403 для foreign origin в fixture» сильнее общего «защита настроена», но всё ещё относится только к модели, не к серверу.
Матрица разбора: симптом → причина → проверка → действие
| Симптом | Вероятная причина | Безопасная проверка | Действие |
|---|---|---|---|
| foreign origin не должен менять профиль | origin не включён в gate либо сравнивается после update | fixture подаёт https://other.example.test и ждёт 403 | вернуть origin gate перед побочным эффектом и записать область его применимости |
| пустой token не должен пройти | форма не передала поле либо controller его не сравнил | fixture подаёт пустое csrfToken и ждёт 403 | проверить контракт формы и controller, не ослабляя проверку ради одного клиента |
| пользователь без write права не меняет профиль | session ошибочно принята за permission | fixture подаёт profile:read и ждёт 403 | поставить server-side permission перед update и покрыть соседние write routes отдельно |
| ответ обязан нести известную policy | headers живут отдельно от route или не описаны | fixture сверяет поля CSP и Set-Cookie с учебной картой | проверить конкретный server version и ответ разрешённого стенда до rollout |
Таблица не назначает severity и не делает предположение о внешнем атакующем. Она даёт узкие сигналы. Симптом — что именно ожидалось от одного route; причина — какой слой мог исчезнуть; проверка — что можно выполнить без доступа к сети; действие — куда вернуть контроль. Если fixture отказывает foreign origin, это не измеряет защиту browser или proxy. Если она разрешает expected request, это не доказывает, что token имеет хорошую entropy. Каждый результат нужно читать ровно в своей границе.
HTTP-пример нужен для разговора между кодом и конфигурацией
Симптом разрыва часто начинается на стыке команд: backend говорит «token проверен», ops говорит «headers добавлены», а browser request никто не описал. Причина — у route нет общего contract. Проверка — записать безопасный учебный request и response рядом с fixture. Действие — при появлении разрешённого тестового стенда сравнить фактический ответ с этим contract, не подменяя его production cookie или реальным user data.
POST /admin/profile HTTP/1.1
Host: admin.example.test
Origin: https://admin.example.test
Cookie: sid=training-session-42
Content-Type: application/x-www-form-urlencoded
displayName=Alex&csrf=training-csrf-6fd2
HTTP/1.1 204 No Content
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
Set-Cookie: sid=training-session-42; Path=/admin; Secure; HttpOnly
Referrer-Policy: same-origin
Заголовок Origin в этом trace — вход одной учебной HTML-формы, не способ защитить все endpoints. Cookie sid тоже синтетический; RFC 6265 полезен для смысла атрибутов, но не для выдачи готовой session architecture. CSP2 policy в response contract показывает желаемую defence in depth для страницы, но validation и output encoding живут в application code. Это разделение предотвращает типичную ошибку: пытаться починить server-side authorisation браузерным header.
Если разбор показывает неполный response contract, полезно посмотреть на форму конфигурации, но не выдавать её за применённый server state. Следующий фрагмент оставляет /admin/ явной областью policy и не содержит реального upstream, host или certificate. Проверка синтаксиса, соответствия версии nginx и фактического ответа требуют отдельного разрешённого стенда. Действие в этой статье уже: сравнить требуемые поля с fixture и зафиксировать, что интеграционная проверка ещё впереди.
# Учебный фрагмент, не готовая конфигурация production-сервера.
# admin.example.test — учебный origin; внешний адрес или секрет здесь не нужны.
location /admin/ {
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always;
add_header Referrer-Policy "same-origin" always;
proxy_pass http://training_admin_upstream;
}
# Атрибуты cookie выставляет приложение после успешной аутентификации:
# Set-Cookie: sid=...; Path=/admin; Secure; HttpOnly
Фикстура проверяет инварианты, а не ищет дыры
Учебная fixture должна быть скучной. Она берёт известный request, затем меняет ровно одно условие: origin, token или permission. Если после изменения решение остаётся разрешающим, contract потерян. Если меняются сразу три поля, результат трудно объяснить. Поэтому модуль возвращает четыре decisions и отдельную карту headers. У него нет URL fetch, DNS, файлов, cookie jar, browser engine, proxy process или сканера. Это сознательное ограничение не даёт назвать локальную логику security assessment.
# В памяти: сеть, browser, proxy и scanner не запускаются.
node web/scripts/upgrade-2020-11.mjs --verify-fixture
# Ищем не уязвимость, а сохранение учебного контракта:
# expectedRequestAccepted=true
# foreignOriginRejected=true
# missingTokenRejected=true
# insufficientPermissionRejected=true
# cspContractPresent=true
# sessionCookieContractPresent=true
Положительный результат fixture означает шесть узких вещей: expected route допускается; foreign origin, пустой token и недостаточное permission отклоняются; в карте присутствуют CSP и cookie attributes. Он не означает, что nginx синтаксически применил фрагмент, browser получил header, все response status покрыты, настоящее cookie имеет безопасное lifetime или злоумышленник не найдёт другой endpoint. Такой verdict стоит сохранить именно с этой границей — иначе следующий читатель примет учебный unit-level check за проверку периметра.
Как читать отказ, не превращая его в инцидент
Первый случай: foreign origin неожиданно допускается. Причина обычно не в CSP: policy ответа не участвует в server-side decision. Проверка — открыть gate и убедиться, что origin сравнивается до update. Действие — вернуть проверку и добавить fixture для конкретного route. Второй случай: token пуст, но update прошёл. Причина — поле формы не связано с controller или сравнение заменено условием «если поле есть». Проверка — передать пустую строку. Действие — требовать совпадение с ожидаемым server-side значением, не логируя сам token.
Третий случай: user с profile:read меняет профиль. Причина — UI знает о роли, а сервер нет, либо permission проверен для другого действия. Проверка — изменить только permission в synthetic request. Действие — найти настоящий владелец authorisation и поставить gate перед эффектом. Четвёртый случай: response contract не содержит HttpOnly или CSP directive. Причина может быть в карте конфигурации, месте добавления header или в выбранной версии сервера. Проверка — сначала поправить в памяти contract, затем при разрешении проверить один известный response на стенде.
Нумерованный маршрут разбора
- Назвать один актив и один route. Не стартовать с «проверим сайт», если неизвестно, какое состояние защищаем.
- Составить ожидаемый request contract без секретов: method, path, training origin, факт session, token и permission.
- Выбрать один положительный и минимум три отрицательных случая. В каждом отрицательном случае менять одно условие, чтобы причина отказа оставалась читаемой.
- Сверить response contract: cookie attributes, CSP directives и область применения. Не выводить из наличия header факт его доставки живым сервером.
- Запустить fixture, сохранить decision и границу verdict. Если любой gate разрешает изменённый request, остановить rollout этого маршрута и вернуть явную проверку до эффекта.
- Отдельно запланировать разрешённую интеграционную проверку: версия веб-сервера, HTTPS, фактические headers, browser compatibility, logs и остальные routes. Не подменять её этим пакетом.
Граница и следующая зрелость
Этот разбор не сканирует приложение, не выявляет CVE, не проверяет database, passwords, upload, CORS, CSP violations, clickjacking во всех browser, TLS или propagation headers. Он не использует современный AppSec dashboard и не претендует на полноценный threat model. Для ноября 2020 года это нормальная точка развития автора: вместо общих советов появляется один воспроизводимый contract и ясное доказательство каждого маленького gate. Дальше можно добавить разрешённый стенд и новые активы, но только не переписывать этот учебный результат как будто он уже проверил их.
Все origin, cookies, токены, заголовки, имена маршрутов и результаты ниже учебные. Модуль не открывает сеть, не сканирует цели, не проверяет реальный сервер и не хранит секреты.
Проверяемые источники
- OWASP ASVS 4.0.2 — релиз 28 октября 2020 года — историческая версия, доступная в ноябре 2020 года; задаёт проверяемые требования, но не заменяет модель угроз конкретного приложения
- OWASP Top 10:2017 — версия 2017 года; используется как язык для разговора о рисках, а не как готовый список настроек для любого сервера
- RFC 6265 — HTTP State Management Mechanism, апрель 2011 — описывает поля Cookie и Set-Cookie, включая атрибуты Secure, HttpOnly и Path; не превращает один cookie-атрибут в защиту от всех угроз
- W3C Content Security Policy Level 2 — Recommendation, 15 декабря 2016 года — историческая спецификация CSP2: политика передаётся HTTP-заголовком и является дополнительным барьером, а не заменой validation и encoding
- nginx: ngx_http_headers_module — add_header — официальная документация синтаксиса; фрагмент конфигурации применим только после сверки с версией и маршрутизацией конкретного сервера