{ "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. Если связь оборвалась, результатом должен быть отказ от вывода, а не зелёная отметка.

\n

Почему перечень controls вводит в заблуждение

\n

Перечень хранит существительные: CSP, sanitizer, SAST, review, SameSite. Он не хранит направление связи. Из строки «CSP настроен» не следует, что конкретный фрагмент не попадёт в опасный sink. Из строки «cookie защищена» не следует, что сервер проверяет намерение запроса. Из строки «сканер чист» не следует, что сканер видел нужную ветку, конфигурацию и версию приложения.

\n

Один control часто действует позднее источника проблемы. CSP может ограничить исполнение скрипта в документе. Он не исправляет неверную валидацию и не превращает небезопасный HTML-sink в безопасный. Поэтому связь надо записать глаголом: отклонить скрипт без известного nonce на границе документа. Такой текст уже можно сопоставить с шагом атаки и с проверкой.

\n
\"Матрица
Список мер превращается в инженерное утверждение только после четырёх связей: что атакуется, где путь прерывается, что наблюдалось и что осталось вне проверки.
\n

Минимальная модель пути

\n

Рассмотрим учебный путь. Ненадёжный фрагмент достигает именованного sink, а sink формирует документ, в котором возможен исполняемый сценарий. В модели это три разных шага. Нельзя заменить их словом «XSS»: короткое название скрывает условие и место, где должна сработать защита.

\n
asset: 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

Симптом → причина → проверка → действие

\n
Диагностика разорванной трассировки
СимптомПричинаПроверкаДействие
«CSP включён», но путь атаки не названControl записан без interruptionПопросить назвать шаг, который он прерываетДобавить ordered path и точку отказа
Тест зелёный, но относится к другой страницеEvidence не связано с threat idСверить идентификаторы пути и наблюденияОстановить вывод и привязать проверку заново
После исправления исчезла строка residual riskПоздний control приняли за исправление источникаПроверить validation, encoding и все sinksВернуть открытые участки и следующий вопрос
В отчёте написано «защита гарантирована»Ограниченное evidence расширили риторикойСопоставить каждое слово с наблюдениемСузить утверждение до проверяемого факта
Неизвестно, что делать при разрыве связиУ модели нет отрицательной веткиПодать запись без path или bindingВернуть точный stop и недостающий вход
\n

Как выглядит отрицательный путь

\n

Отрицательная ветка важнее красивого положительного результата. Если evidence содержит наблюдение, но ссылается на другой threat или control, система не должна искать «похожую» запись по тексту. Она возвращает ошибку связи. Если attack path пуст, нельзя считать policy доказанной. Если residual risk не содержит открытого участка и вопроса для следующей проверки, положительный hand-off также нельзя принимать.

\n
const 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-записи. Проверку браузерного поведения, серверного ответа и входных данных проводят отдельно.

\n

Что считается evidence

\n

Evidence — не обязательно скриншот или лог. Это ограниченное наблюдение, которому заранее задан допустимый вывод. Для учебного объекта допустимы два факта: именованные директивы присутствуют в записи и отрицательный сценарий без nonce не авторизован в этой модели. Нельзя из них выводить отсутствие XSS, корректность всех HTML-преобразований, защиту сессии или безопасность каждого браузера.

\n

Границу пишут рядом с observation. Иначе при передаче она исчезает, а фраза «negative case не авторизован» превращается в «уязвимость устранена». Хорошая запись отвечает на четыре вопроса: какой threat проверялся, какой control к нему привязан, что именно наблюдалось и чего это наблюдение не доказывает.

\n

Эта дисциплина помогает и при конфликте controls. Санитизация может менять вход, а CSP — ограничивать последствия в документе. У них разные interruption. Если evidence относится только к policy, нельзя выдать вывод о преобразовании данных. Если два controls действуют на разные шаги, их нельзя слить в одну зелёную строку только ради компактного отчёта.

\n

Порядок проверки

\n
  1. Назовите актив. Запишите, что защищаете: например, контекст рендера браузера, профиль пользователя или изменение счёта.
  2. Опишите условие. Укажите, при каком входе или состоянии путь становится возможным.
  3. Разложите маршрут. Дайте шагам порядок и действующие глаголы. Не заменяйте маршрут названием класса уязвимости.
  4. Назовите interruption. Укажите, какой control и каким решением прерывает конкретный шаг.
  5. Привяжите evidence. Свяжите наблюдение одновременно с threat id и control id.
  6. Запишите предел. Явно перечислите утверждения, которых наблюдение не поддерживает.
  7. Оставьте residual risk. Назовите открытые участки и вопрос для отдельной проверки.
  8. Проверьте отрицательную ветку. Убедитесь, что неизвестный путь, чужая связь и чрезмерный положительный вывод дают stop.
\n

Ограничения механизма

\n

Трассировка не оценивает вероятность атаки, ущерб, exploitability или полноту покрытия. Она не заменяет threat modeling, code review, тесты, статический анализ, сканирование и независимую оценку. Она только не даёт одной записи присвоить себе результаты всех этих методов.

\n

CSP не заменяет валидацию входа и кодирование вывода. Cookie-флаги не заменяют проверку полномочий и намерения операции. CORS не является универсальной защитой от CSRF: если сервер принимает изменяющий запрос с cookie без отдельной проверки, исправление CORS может не закрыть путь. Этот отрицательный пример применим только при соответствующей схеме браузера, cookie и серверного endpoint; его надо проверять реальным запросом в тестовой среде.

\n

Описанный JavaScript ограничен учебной моделью. Он не сообщает production-результаты и не даёт разрешения на релиз. Если нужен реальный вывод, потребуются отдельные входы: собранный response, конфигурация доставки policy, тестовый браузер, тестовые данные и зафиксированный scope. Нельзя подменить их одной записью в памяти.

\n

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

\n

Материал ревью готов, когда независимый читатель может пройти цепочку от актива до residual risk без устных пояснений. Для каждого control видны threat id, ordered path и точка прерывания. Для каждого evidence видны два binding id и допустимый вывод. Открытые участки не скрыты. Отрицательные случаи возвращают определённый stop. Положительная ветка остаётся только ограниченной передачей на следующий review, а не разрешением на изменение системы.

\n

Практический тест занимает один проход: удалите из записи любое звено и снова запустите валидатор. Если запись всё ещё выглядит «зелёной», модель слишком либеральна. Добавьте проверку, которая останавливает её на конкретном разрыве. Так список controls превращается в проверяемый аргумент, а не в коллекцию обещаний.

\n

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

\n" }