Files
progcode/editorial/agent-rewrites/062.json
T

8 lines
23 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 62,
"slug": "editorial-2026-04-mechanism-modern-web-security",
"title": "Почему список security controls не доказывает защиту веб-приложения",
"excerpt": "CSP, HttpOnly, CORS и сканер отвечают на разные вопросы. Разбираем путь от угрозы к точке прерывания, связываем его с проверяемым evidence и оставляем остаточный риск там, где данных недостаточно.",
"contentHtml": "<p>Проблема security review часто обнаруживается не в отсутствии мер, а в слишком сильном выводе. В отчёте стоят галочки «CSP включён», «cookie с HttpOnly», «сканер чист», но никто не может показать, какой запрос или фрагмент данных остановил каждый control. Если после этого endpoint меняет профиль по чужому POST, список мер создаёт ложное ощущение закрытой уязвимости.</p>\n<p>Удобнее разложить проверку на четыре связанных вопроса: какой asset защищаем, как выглядит ordered attack path, где control прерывает путь и что именно наблюдалось. Последняя часть — evidence, то есть воспроизводимое подтверждение с ограниченным выводом. Всё, что не проверено, остаётся residual risk. Такая схема не обещает безопасность всего приложения, зато не позволяет одной настройке присвоить себе результат чужого теста.</p>\n<h2>Список мер не хранит причинность</h2>\n<p>Строка «CORS настроен» описывает настройку чтения ответа из браузера, но не отвечает, принял ли сервер изменяющий запрос. HttpOnly не даёт JavaScript прочитать cookie, но браузер по-прежнему может приложить её к запросу. CSP ограничивает разрешённые ресурсы и выполнение скриптов в документе, но не исправляет небезопасный HTML-sink. Даже хороший SAST-анализ говорит только о просмотренных файлах, правилах и версии запуска.</p>\n<p>Один и тот же control может быть полезен в одном месте и бесполезен в другом. Поэтому вместо существительного нужен глагол: «сервер отклоняет POST без действительного CSRF-токена до записи в профиль». В этой формулировке видны операция, условие отказа и момент, до которого должен сохраниться asset. Её можно сопоставить с тестом. «CSRF включён» сопоставить не с чем.</p>\n<figure><img src=\"/assets/illustrations/vibe-web.svg\" alt=\"Схематичное окно с веб-инструментами: статические файлы, JSON и сетевые механизмы рядом с HTTP, WebSocket, сессиями и шаблонами\" loading=\"lazy\" /><figcaption>Безопасность веба пересекает несколько границ. Иллюстрация перечисляет соседние механизмы, но сама по себе не доказывает их настройку; доказательством становится только привязанный к конкретной операции тест.</figcaption></figure>\n<h2>Один путь показывает разницу между CORS и CSRF</h2>\n<p>Возьмём изменение email. Пользователь вошёл в <code>bank.example</code>, браузер хранит cookie сессии, а endpoint принимает <code>POST /api/profile/email</code>. Внешняя страница может попытаться отправить такой запрос с cookie. CORS определяет, может ли внешний origin прочитать ответ через браузерный API. CSRF-защита должна не допустить нежелательное изменение состояния на сервере.</p>\n<pre><code>&lt;form action=&quot;https://bank.example/api/profile/email&quot; method=&quot;POST&quot;&gt;\n &lt;input name=&quot;email&quot; value=&quot;attacker@example.net&quot;&gt;\n&lt;/form&gt;\n&lt;script&gt;document.forms[0].submit()&lt;/script&gt;\n\n// Сценарий приведён для анализа потока.\n// Не запускайте его против чужого сервиса.</code></pre>\n<p>Если endpoint доверяет только cookie, чужой запрос может дойти до операции записи независимо от того, увидит ли внешний сайт тело ответа. Проверка заголовка <code>Origin</code>, synchronizer token или другой серверный механизм может прервать путь. Свойство <code>SameSite</code> уменьшает часть cross-site-сценариев, но зависит от контекста запроса и политики cookie. Ни один из этих controls нельзя объявлять заменой аутентификации или авторизации.</p>\n<p>Здесь полезно записать путь по шагам: <code>external-page → browser-attaches-cookie → POST-email → profile-changed</code>. CORS относится к чтению ответа, CSRF-проверка — к допуску изменяющего запроса, авторизация — к праву менять конкретный профиль. Если evidence проверяет только заголовок ответа, из него нельзя выводить, что запись не произошла.</p>\n<h2>Как связать threat, control и evidence</h2>\n<p>Для каждой операции составьте маленькую карточку. <code>asset</code> — изменяемый объект. <code>precondition</code> — условие, при котором путь возможен. <code>attackPath</code> — упорядоченные действия. <code>interruption</code> — точный запрет. <code>evidence</code> — наблюдение, которое можно повторить. Поле <code>notProven</code> защищает от расширения вывода при пересказе.</p>\n<pre><code>{\n &quot;asset&quot;: &quot;profile.email&quot;,\n &quot;precondition&quot;: &quot;valid_session_cookie&quot;,\n &quot;attackPath&quot;: [\n &quot;external_page&quot;,\n &quot;browser_attaches_cookie&quot;,\n &quot;POST_profile_email&quot;,\n &quot;profile_changed&quot;\n ],\n &quot;interruption&quot;: &quot;reject_without_csrf_token_before_write&quot;,\n &quot;evidence&quot;: &quot;403_and_email_unchanged_in_test_environment&quot;,\n &quot;notProven&quot;: [\n &quot;all_other_write_endpoints&quot;,\n &quot;authorization_for_other_profiles&quot;,\n &quot;production_proxy_behavior&quot;\n ]\n}</code></pre>\n<p>Карточка не является security verdict. Она фиксирует область одного утверждения. Если проверка вернула <code>403</code>, но после запроса email изменился, статус ответа не спасает вывод: контроль сработал слишком поздно или тест смотрит не на тот asset. Если идентификатор endpoint отсутствует, карточку нельзя переносить на весь API.</p>\n<h2>Воспроизводимый тест серверной границы</h2>\n<p>Проверять нужно не только ответ, но и состояние после отказа. Ниже — команда для локального или специально разрешённого тестового сервиса. Подставьте адрес своего стенда и тестовую cookie; домен из примера не является целью для запроса.</p>\n<pre><code>BASE=http://127.0.0.1:3000\n\ncurl --fail-with-body -i -X POST &quot;$BASE/api/profile/email&quot; \\\n -H 'Origin: https://attacker.example' \\\n -H 'Content-Type: application/x-www-form-urlencoded' \\\n -b 'session=TEST_SESSION' \\\n --data 'email=attacker@example.net'\n\n# Ожидание для защищённого тестового endpoint:\n# HTTP/1.1 403 Forbidden\n# и прежнее значение profile.email после запроса.</code></pre>\n<p>Команда воспроизводима только при известных маршруте, тестовой сессии и контракте ответа. <code>TEST_SESSION</code> не должен быть настоящим секретом. Если сервис возвращает <code>401</code>, сначала не создана аутентифицированная тестовая сессия; это не доказательство CSRF-защиты. Для положительной ветки повторите запрос с действительным CSRF-токеном, сохраните новый ответ и отдельно подтвердите ожидаемое изменение email.</p>\n<p>Минимальный псевдокод показывает место отказа. В реальном приложении названия функций, хранилище токенов и формат ошибок будут другими.</p>\n<pre><code>app.post('/api/profile/email', async (req, res) =&gt; {\n const session = await getSession(req);\n if (!session) return res.sendStatus(401);\n\n if (!sameOrigin(req.get('Origin')) ||\n !validCsrfToken(req.get('X-CSRF-Token'), session)) {\n return res.sendStatus(403);\n }\n\n const email = parseEmail(req.body.email);\n if (!email) return res.sendStatus(400);\n\n if (!(await canEditProfile(session.userId, req.body.profileId))) {\n return res.sendStatus(403);\n }\n\n await updateEmail(req.body.profileId, email);\n return res.sendStatus(204);\n});</code></pre>\n<p>Порядок здесь важен: сессия, намерение запроса, формат значения, право на объект, затем запись. Это не универсальный middleware и не готовая библиотека. Он не показывает, как сравнивать секреты, ротировать токены, проводить логирование или защищать другие каналы. Эти детали нужно проверять по фреймворку и коду конкретного сервиса.</p>\n<h2>Что именно считать evidence</h2>\n<p>Evidence — это не обязательно скриншот. Им может быть HTTP-ответ вместе с проверкой состояния, тестовый лог с correlation id, результат статического анализа с зафиксированным scope или браузерное наблюдение для конкретной политики. У каждого наблюдения должен быть допустимый вывод. Например, ответ <code>403</code> на POST без токена подтверждает отказ этой ветки на данном endpoint в данном окружении. Он не подтверждает защиту всех mutations.</p>\n<p>Записывайте границу рядом с результатом. «В тесте запрос без токена получил 403, запись не изменилась; не проверены фоновые задачи, соседние маршруты и production proxy» — полезное утверждение. «CSRF закрыт» — уже нет. Точно так же наличие <code>Content-Security-Policy</code> показывает доставку заголовка. Оно не доказывает, что каждый пользовательский фрагмент безопасно закодирован и ни один sink не исполняет данные.</p>\n<table><caption>От наблюдаемого симптома к следующему действию</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Что он означает</th><th scope=\"col\">Проверка</th><th scope=\"col\">Следующее действие</th></tr></thead><tbody><tr><td>Чужой origin получает CORS error</td><td>Браузер ограничил чтение ответа</td><td>Проверить, изменилось ли состояние после отдельного POST</td><td>Оставить серверную CSRF-проверку, если операция использует cookie</td></tr><tr><td>Cookie имеет HttpOnly</td><td>Скрипт не читает её значение через API браузера</td><td>Проверить фактический запрос и серверную проверку намерения</td><td>Не выдавать HttpOnly за защиту от CSRF</td></tr><tr><td>POST без токена вернул 403</td><td>Одна отрицательная ветка остановлена</td><td>Сверить asset, endpoint, окружение и состояние после запроса</td><td>Записать scope; отдельно проверить другие write-маршруты</td></tr><tr><td>Токен есть, но меняется чужой профиль</td><td>CSRF не заменяет авторизацию</td><td>Повторить с объектом другого пользователя</td><td>Проверить владельца ресурса до записи</td></tr><tr><td>CSP есть, но пользовательский HTML исполняется</td><td>Защита документа не исправила источник или sink</td><td>Проверить вывод, кодирование и response policy для этой страницы</td><td>Исправить безопасный рендеринг; CSP оставить дополнительным слоем</td></tr></tbody></table>\n<h2>Почему CSP не исправляет XSS в одиночку</h2>\n<p>W3C описывает Content Security Policy как механизм управления ресурсами и выполнением, который снижает риск content injection и работает как defense-in-depth. Там же явно сказано, что CSP не заменяет внимательную валидацию входа и кодирование вывода. Поэтому в трассе CSP может прерывать попытку выполнить неразрешённый скрипт, но не доказывает, что строка безопасно попала в HTML, атрибут или DOM-sink.</p>\n<p>Для одного пользовательского поля нужны разные проверки: допустимый формат на сервере, безопасный контекст вывода и отрицательный тест для конкретной точки рендера. Политика с <code>script-src</code> и nonce относится к браузерному исполнению. Она не даёт права убрать тесты для шаблона или клиентского кода. Если policy меняется, повторно проверьте доставленный заголовок и позитивные сценарии приложения: чрезмерно строгая политика может ломать legitimate scripts, а режим <code>Report-Only</code> сообщает о нарушениях, но не блокирует их.</p>\n<h2>Порядок проверки без ложного зелёного статуса</h2>\n<ol><li><strong>Выберите одну операцию.</strong> Назовите endpoint, метод, asset и изменяемое поле.</li><li><strong>Опишите угрозу.</strong> Запишите precondition и цену ошибки, если запись пройдёт.</li><li><strong>Разложите путь.</strong> Перечислите источник запроса, cookie, endpoint, проверки и побочный эффект.</li><li><strong>Разделите controls.</strong> Для CORS, CSRF, авторизации, валидации и CSP укажите разные interruption.</li><li><strong>Сделайте отрицательный запрос.</strong> Уберите токен, смените origin или выберите чужой объект в разрешённом стенде.</li><li><strong>Проверьте состояние.</strong> Статус отказа недостаточен: убедитесь, что asset не изменился.</li><li><strong>Проверьте положительную ветку.</strong> С действительными данными операция должна пройти согласно контракту.</li><li><strong>Сверьте покрытие.</strong> Найдите остальные state-changing маршруты, фоновые обработчики и альтернативные протоколы.</li><li><strong>Запишите остаток.</strong> Перечислите непроверенные браузеры, proxy, cookie-контексты, sinks и каналы.</li></ol>\n<h2>Ограничения применимости</h2>\n<p>Трассировка threat → control → evidence не оценивает вероятность атаки, ущерб, exploitability или полноту покрытия. Она не заменяет threat modeling, code review, динамические тесты, SAST, сканирование и независимую оценку. Её задача уже: не дать отчёту расширить узкое наблюдение до утверждения о всей системе.</p>\n<p>Пример с cookie относится к state-changing HTTP endpoint. Он не покрывает OAuth callback, WebSocket, GraphQL mutation, загрузку файла, очередь сообщений или действия, авторизованные не cookie, а другим способом. Для каждого канала нужно построить отдельный путь. Для файла дополнительно проверяют тип, содержимое, имя, место хранения и выдачу. Для очереди — отправителя, обработчика, повторы и идемпотентность.</p>\n<p>Команды и псевдокод выше не дают разрешения атаковать реальный сервис. Выполняйте проверки только на локальном стенде или при явном разрешении владельца. Если неизвестно, какой middleware обслуживает маршрут, или нет способа проверить состояние после отказа, результат должен быть «нужна отдельная проверка», а не «защищено».</p>\n<h2>Критерий готового security review</h2>\n<p>Для выбранной операции читатель должен без устных пояснений увидеть asset, precondition, ordered path, interruption, evidence и residual risk. Отрицательный запрос должен получить ожидаемый отказ до изменения состояния, положительный — пройти по контракту. Запись должна назвать окружение и перечислить, что не проверялось. Только такой результат можно передать следующему инженеру как ограниченное evidence; это не сертификат безопасности всего приложения.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://www.w3.org/TR/CSP3/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C: Content Security Policy Level 3</a> — официальная спецификация описывает CSP как механизм контроля ресурсов и выполнения и прямо ограничивает её роль дополнительной защитой. На странице указан статус Working Draft; это не доказательство поведения конкретного браузера или сайта.</li><li><a href=\"https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html\" target=\"_blank\" rel=\"noopener noreferrer\">OWASP: Cross-Site Request Forgery Prevention Cheat Sheet</a> — официальный практический материал о synchronizer token, signed double-submit cookie, проверке Origin и ограничениях SameSite. Он не подтверждает, что конкретный endpoint применяет эти правила.</li><li><a href=\"https://owasp.org/www-project-application-security-verification-standard/\" target=\"_blank\" rel=\"noopener noreferrer\">OWASP Application Security Verification Standard</a> — официальная страница стандарта с проверяемыми требованиями к безопасности приложений. Само наличие требования не является результатом assessment.</li><li><a href=\"https://csrc.nist.gov/pubs/ir/8397/final\" target=\"_blank\" rel=\"noopener noreferrer\">NISTIR 8397: Guidelines on Minimum Standards for Developer Verification of Software</a> — публикация NIST перечисляет threat modeling, тесты, статический анализ и другие техники developer verification. Она не заменяет evidence для отдельного маршрута.</li></ul>"
}