Симптом неприятный: в release notes перечислены cookie flags и CSP, но старый POST всё равно меняет данные после одной проверки «пользователь залогинен». Цена — ложная уверенность. При следующей ошибке команда будет обсуждать, какой header добавить, хотя отсутствует permission, token сравнивается после update или evidence вообще не связано с конкретным route. Набор защитных слов в конфигурации не показывает, где запрос должен остановиться и кто владеет решением.
Я разберу учебную границу ноября 2020 года, а не современную security platform. У нас есть одна HTML-форма на https://admin.example.test, синтетическая сессия и один предполагаемый эффект — изменение display name. Fixture проверяет решения до такого эффекта. Маршрут проходит через proxy contract, application gates и ответ. Внутри этой статьи нет реального HTTP client, browser, server, scanner или внешней цели. Механизм нужен, чтобы сначала увидеть связь «актив → угроза → контроль → доказательство», а потом уже обсуждать более широкий риск.
У запроса несколько разных владельцев
Сессия отвечает на вопрос «кого сервер сейчас узнаёт». Token и origin отвечают на более узкий вопрос: «похож ли этот state change на ожидаемый путь формы». Permission отвечает на вопрос «может ли этот субъект менять именно это состояние». CSP и cookie attributes описывают ограничения для ответа и user agent. Эти ответы нельзя заменить друг другом. Если есть session, но нет profile:write, update не должен начаться. Если есть permission, но token отсутствует, HTML-маршрут с такой моделью тоже не должен начаться.
Причина путаницы в том, что все проверки часто называют «auth». Проверка против этого — назвать вход, владельца и момент. Cookie читает и возвращает user agent по правилам HTTP state management; приложение интерпретирует сессию; application code принимает решение о permission; web server добавляет policy header. Действие — вынести каждую границу в короткий contract, чтобы после изменения было видно, какой именно слой ослабили или не проверили.
Карта границ для одного POST
| Шаг | Вход | Решение | Доказательство в пакете |
|---|---|---|---|
| Маршрут | POST /admin/profile | этот contract применим только к одной форме | fixture отклоняет другой path как несоответствие маршруту |
| Сессия | sid из учебного cookie | известен ли субъект | неизвестная session возвращает 401 в модели |
| Контекст формы | Origin и CSRF token | похоже ли действие на ожидаемый маршрут | foreign origin и пустой token дают 403 |
| Авторизация | profile:write | может ли субъект менять профиль | permission profile:read даёт 403 |
| Ответ | CSP, Set-Cookie, Referrer-Policy | какие browser-facing ограничения должен получить маршрут | fixture проверяет наличие заданных полей, а не delivery реального сервера |
В таблице намеренно разделены status 401 и 403 модели. Это не универсальный протокол для каждого API; это способ не потерять смысл учебного решения: сессия не узнана либо субъект узнан, но одно из условий запрещает действие. Проект может выбрать другие сообщения, error shape и audit log. Главное — не менять итог на «успех» ради того, чтобы клиенту было удобнее, и не продолжать update после неудачного gate.
Cookie contract — это транспорт состояния, не право
RFC 6265 фиксирует, что server задаёт состояние через Set-Cookie, а user agent возвращает его в Cookie. Атрибут Secure связывает учебный cookie с HTTPS-сценарием; HttpOnly ограничивает доступ script к cookie; Path=/admin делает область отправки уже. Симптом неверной модели — считать это полноценной authorisation. Причина — cookie легко выглядит как идентичность пользователя. Проверка — найти отдельный server-side permission gate. Действие — не разрешать побочный эффект, пока этот gate не сказал «да».
// Карта ответа для учебного контракта; значения не отправляются в сеть.
const headers = createTrainingResponseHeaders();
headers['set-cookie'];
// sid=training-session-42; Path=/admin; Secure; HttpOnly
headers['content-security-policy'];
// default-src 'self'; script-src 'self'; object-src 'none';
// base-uri 'self'; frame-ancestors 'none'
У cookie есть и другая граница: атрибуты не защищают сервер от ошибки в назначении permission. Они не доказывают корректную реализацию logout, срок жизни сессии, rotation ID, HTTPS redirect или настройку доменов. В конкретном приложении эти вопросы требуют отдельных требований и тестов. Для автора уровня М3 важнее не перечислить всё сразу, а не выдать небольшой transport contract за готовую authentication-систему.
CSP ограничивает контент, но не чинит источник инъекции
Симптом после добавления CSP часто такой: страница перестала грузить нужный script, и policy ослабляют до широкого allowlist. Причина — до настройки никто не выписал, откуда реально приходят scripts, styles, images и frames. Проверка — начать с одной страницы и inventory её ресурсов, а не добавлять домены по ошибке в консоли. Действие — оставить policy минимальной для известного маршрута, выбрать режим развёртывания для своего сервера и отдельно чинить input validation и output encoding в приложении.
CSP Level 2, опубликованный W3C в 2016 году, описывает передачу policy через HTTP header и прямо рассматривает CSP как defense in depth для content injection. Это важно для исторического тона: в 2020 году policy уже полезна, но она не даёт разрешения игнорировать серверный код. В учебном contract object-src 'none' убирает plugin content, base-uri 'self' ограничивает base URL, а frame-ancestors 'none' запрещает встраивание. Применимость конкретных directives надо проверять на поддерживаемых user agents и настоящем наборе assets.
# Учебный фрагмент, не готовая конфигурация 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
Фрагмент показывает расположение policy рядом с областью /admin/, а не гарантирует поведение любой сборки nginx. Он не содержит реальный upstream address, certificate, secret или hostname. Перед использованием нужна версия сервера, staging-route и фактический список ресурсов. Если page зависит от inline code, внешнего analytics host или embedded partner frame, правильное действие — описать зависимость и решить, нужна ли она активу, а не молча расширить policy до любой сети.
Токен и origin — условия одного конкретного контекста
Симптом CSRF-защиты без контракта — token сравнивается в одном controller, но другой state-changing endpoint забыли включить. Причина — защита привязана к кусочку формы, а не к карте маршрутов. Проверка — выписать все изменения состояния и для каждого решить, какие клиенты его вызывают. Действие — для этой учебной HTML-формы требовать origin и token до update, а для API отдельно задокументировать свой способ аутентификации и угрозы.
Origin не следует применять как универсальный заменитель token: protocol может не послать его в каждом контексте, а у API могут быть другой client и другой contract. Token также не заменяет permission. В нашей маленькой модели это два независимых условия, поэтому fixture проверяет их раздельно. Такой пример не советует угадывать стратегию для всех endpoints; он показывает, что у контекста формы есть собственные входы, и их отказ должен быть наблюдаем.
HTTP trace и fixture проверяют разные слои
HTTP trace полезен, когда backend и web-server configuration должны договориться об одном маршруте. Fixture полезна, когда нужно не потерять ветвление application code при следующем refactor. Симптом ошибки — использовать один артефакт вместо другого: читать красивый header как доказательство permission или принимать unit-level 403 за факт delivery от proxy. Проверка — хранить оба коротких примера. Действие — сначала сравнить in-memory decisions, а потом на разрешённом стенде проверить фактический response.
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
# В памяти: сеть, browser, proxy и scanner не запускаются.
node web/scripts/upgrade-2020-11.mjs --verify-fixture
# Ищем не уязвимость, а сохранение учебного контракта:
# expectedRequestAccepted=true
# foreignOriginRejected=true
# missingTokenRejected=true
# insufficientPermissionRejected=true
# cspContractPresent=true
# sessionCookieContractPresent=true
В этой паре нет network operation. Raw request существует только как текстовый contract, а fixture вызывает функцию напрямую с обычными объектами. Поэтому она не тестирует HTTP parser, cookie jar, TLS, nginx, browser policy или CORS. Зато она ясно показывает, какие поля меняют решение application layer. Для следующего шага это полезнее неявного «security middleware включён».
Нумерованный путь построения baseline
- Зафиксировать один актив и один state-changing route. Описать клиентов маршрута, но не смешивать HTML-форму с API только потому, что оба используют HTTP.
- Разложить маршрут на владельцев решений: session, контекст формы, permission, response policy. Для каждого назвать вход и время, когда он проверяется.
- Поставить gates перед побочным эффектом и выбрать ясный отрицательный исход. Не оставлять permission после вызова update и не считать UI-кнопку контролем.
- Описать cookie и CSP как ответный contract. Сверить версии browser и web server, список assets и необходимость каждой директивы до enforcement.
- Добавить учебную fixture: положительный request, foreign origin, пустой token и недостаточное permission. Сохранить причины отказа в коротком и не секретном виде.
- Записать непокрытые зоны: реальные headers, TLS, API, upload, логи, recovery, dependency management и отдельные threat models для новых активов.
Что этот механизм не умеет обещать
Механизм не проводит pentest, не обнаруживает уязвимости сети и не заменяет review кода. Он не говорит, что Origin или CSP подходят всем integrations, и не выдаёт учебный token за криптографический secret. Он также не показывает реальную delivery chain: proxy может быть настроен иначе, application может переписать headers, browser может по-своему применить policy. Базовая ценность в другом: у одного запроса появились явно раздельные gates и факт, который можно проверить до подключения более зрелого security процесса.
Все 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 — официальная документация синтаксиса; фрагмент конфигурации применим только после сверки с версией и маршрутизацией конкретного сервера