{ "index": 62, "slug": "editorial-2026-04-mechanism-modern-web-security", "title": "Почему список security controls не доказывает защиту веб-приложения", "excerpt": "Один CSP-заголовок не объясняет, какой шаг атаки он прерывает. Разбираем трассировку threat model → control → evidence → residual risk на учебном примере и фиксируем границу, после которой нужен отдельный тест.", "contentHtml": "
После ревью в отчёте остаётся знакомый список: CSP включён, cookies имеют флаг HttpOnly, сканер не нашёл критических проблем. Через неделю в приложение попадает пользовательский фрагмент, а команда не может ответить, какой именно шаг атаки должен был остановиться. Ошибка стоит дорого: разработчики спорят о настройке заголовка, инцидент получает ложный статус «закрыт», а реальная проверка входных данных и места вывода остаётся без владельца.
\nТезис статьи простой: security control имеет смысл только внутри трассы threat model → interruption → evidence → residual risk. Сначала нужно назвать актив, условие и порядок шагов атаки. Затем — указать, какой control прерывает конкретный шаг. После этого — ограничить вывод наблюдаемым evidence. Всё, что осталось за границей наблюдения, записывают как residual risk. Если связь оборвалась, результатом должен быть отказ от вывода, а не зелёная отметка.
Перечень хранит существительные: CSP, sanitizer, SAST, review, SameSite. Он не хранит направление связи. Из строки «CSP настроен» не следует, что конкретный фрагмент не попадёт в опасный sink. Из строки «cookie защищена» не следует, что сервер проверяет намерение запроса. Из строки «сканер чист» не следует, что сканер видел нужную ветку, конфигурацию и версию приложения.
\nОдин control часто действует позднее источника проблемы. CSP может ограничить исполнение скрипта в документе. Он не исправляет неверную валидацию и не превращает небезопасный HTML-sink в безопасный. Поэтому связь надо записать глаголом: отклонить скрипт без известного nonce на границе документа. Такой текст уже можно сопоставить с шагом атаки и с проверкой.
\nРассмотрим учебный путь. Ненадёжный фрагмент достигает именованного sink, а sink формирует документ, в котором возможен исполняемый сценарий. В модели это три разных шага. Нельзя заменить их словом «XSS»: короткое название скрывает условие и место, где должна сработать защита.
\nasset: browser rendering context\nprecondition: untrusted fragment reaches named sink\npath:\n 1. untrusted-fragment\n 2. unsafe-render-sink\n 3. script-capable-document\ncontrol: reject-script-without-fixed-nonce\nevidence:\n - named-directives-present\n - synthetic-negative-script-is-not-authorized\nЭто учебная запись в памяти. Она не создаёт HTTP-ответ, не запускает браузер, не проверяет заголовок и не измеряет поведение сайта. Её задача — показать форму рассуждения. В реальном ревью такую модель надо связать с конкретным маршрутом, кодом рендера, способом доставки policy и воспроизводимой проверкой.
\nУ control есть узкое место действия. В примере policy может ограничить следующий шаг: браузер не авторизует сценарий без нужного nonce. Но evidence не говорит, откуда взялся фрагмент, корректно ли закодирован вывод и все ли sinks покрыты. Эти вопросы остаются открытыми. Наличие residual risk не означает провал всей защиты. Оно означает, что вывод не расширяют за пределы наблюдения.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| «CSP включён», но путь атаки не назван | Control записан без interruption | Попросить назвать шаг, который он прерывает | Добавить ordered path и точку отказа |
| Тест зелёный, но относится к другой странице | Evidence не связано с threat id | Сверить идентификаторы пути и наблюдения | Остановить вывод и привязать проверку заново |
| После исправления исчезла строка residual risk | Поздний control приняли за исправление источника | Проверить validation, encoding и все sinks | Вернуть открытые участки и следующий вопрос |
| В отчёте написано «защита гарантирована» | Ограниченное evidence расширили риторикой | Сопоставить каждое слово с наблюдением | Сузить утверждение до проверяемого факта |
| Неизвестно, что делать при разрыве связи | У модели нет отрицательной ветки | Подать запись без path или binding | Вернуть точный stop и недостающий вход |
Отрицательная ветка важнее красивого положительного результата. Если evidence содержит наблюдение, но ссылается на другой threat или control, система не должна искать «похожую» запись по тексту. Она возвращает ошибку связи. Если attack path пуст, нельзя считать policy доказанной. Если residual risk не содержит открытого участка и вопроса для следующей проверки, положительный hand-off также нельзя принимать.
\nconst review = {\n threatId: 'fixed-html-injection-path-v1',\n controlId: 'fixed-csp-nonce-boundary-v1',\n evidence: {\n bindsThreatId: 'other-path',\n bindsControlId: 'other-control'\n }\n};\n\nif (review.evidence.bindsThreatId !== review.threatId ||\n review.evidence.bindsControlId !== review.controlId) {\n return {\n status: 'stop-unbound-evidence',\n nextAction: 'bind-observation-to-named-threat-and-control'\n };\n}\nПример синтетический. Он проверяет только равенство идентификаторов в объекте. Он не доказывает свойства браузера и не подтверждает, что приложение послало нужный заголовок. В реальном коде эта проверка может быть частью валидатора review-записи. Проверку браузерного поведения, серверного ответа и входных данных проводят отдельно.
\nEvidence — не обязательно скриншот или лог. Это ограниченное наблюдение, которому заранее задан допустимый вывод. Для учебного объекта допустимы два факта: именованные директивы присутствуют в записи и отрицательный сценарий без nonce не авторизован в этой модели. Нельзя из них выводить отсутствие XSS, корректность всех HTML-преобразований, защиту сессии или безопасность каждого браузера.
\nГраницу пишут рядом с observation. Иначе при передаче она исчезает, а фраза «negative case не авторизован» превращается в «уязвимость устранена». Хорошая запись отвечает на четыре вопроса: какой threat проверялся, какой control к нему привязан, что именно наблюдалось и чего это наблюдение не доказывает.
\nЭта дисциплина помогает и при конфликте controls. Санитизация может менять вход, а CSP — ограничивать последствия в документе. У них разные interruption. Если evidence относится только к policy, нельзя выдать вывод о преобразовании данных. Если два controls действуют на разные шаги, их нельзя слить в одну зелёную строку только ради компактного отчёта.
\nТрассировка не оценивает вероятность атаки, ущерб, exploitability или полноту покрытия. Она не заменяет threat modeling, code review, тесты, статический анализ, сканирование и независимую оценку. Она только не даёт одной записи присвоить себе результаты всех этих методов.
\nCSP не заменяет валидацию входа и кодирование вывода. Cookie-флаги не заменяют проверку полномочий и намерения операции. CORS не является универсальной защитой от CSRF: если сервер принимает изменяющий запрос с cookie без отдельной проверки, исправление CORS может не закрыть путь. Этот отрицательный пример применим только при соответствующей схеме браузера, cookie и серверного endpoint; его надо проверять реальным запросом в тестовой среде.
\nОписанный JavaScript ограничен учебной моделью. Он не сообщает production-результаты и не даёт разрешения на релиз. Если нужен реальный вывод, потребуются отдельные входы: собранный response, конфигурация доставки policy, тестовый браузер, тестовые данные и зафиксированный scope. Нельзя подменить их одной записью в памяти.
\nМатериал ревью готов, когда независимый читатель может пройти цепочку от актива до residual risk без устных пояснений. Для каждого control видны threat id, ordered path и точка прерывания. Для каждого evidence видны два binding id и допустимый вывод. Открытые участки не скрыты. Отрицательные случаи возвращают определённый stop. Положительная ветка остаётся только ограниченной передачей на следующий review, а не разрешением на изменение системы.
\nПрактический тест занимает один проход: удалите из записи любое звено и снова запустите валидатор. Если запись всё ещё выглядит «зелёной», модель слишком либеральна. Добавьте проверку, которая останавливает её на конкретном разрыве. Так список controls превращается в проверяемый аргумент, а не в коллекцию обещаний.
\n