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

8 lines
25 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": 63,
"slug": "editorial-2026-04-practice-modern-web-security",
"title": "Безопасность веба начинается с границы: как доказать, что контроль прерывает атаку",
"excerpt": "CSP, CSRF-токен, CORS и проверка прав останавливают разные шаги атаки. Разбираем карту пути, рабочий пример с nonce, отрицательный тест и границы того, что действительно доказано.",
"contentHtml": "<p>Симптом знакомый: в ответе есть CSP, cookie помечена <code>HttpOnly</code>, API настроил CORS, а endpoint изменения профиля проверяет сессию. В отчёте появляется фраза «защита включена». Но команда не может ответить на более узкий вопрос: какой контроль должен прервать конкретный путь атаки и какое наблюдение это подтверждает? Без такого ответа тест заголовка легко принять за доказательство защиты данных.</p>\n<p>Цена ошибки — не только уязвимость. Во время инцидента приходится заново выяснять, проходили ли входные данные через небезопасный sink (место вывода или выполнения), отправлялась ли cookie, менялось ли состояние и на каком слое ожидали отказ. Практичнее разделить четыре объекта: <strong>asset</strong> — что защищаем, <strong>attack path</strong> — как к нему добираются, <strong>control</strong> — какой переход запрещаем, и <strong>evidence</strong> — что можно повторить и увидеть.</p>\n<h2>Начните с наблюдаемого пути, а не со списка заголовков</h2>\n<p>Возьмём учебный маршрут профиля. Пользователь вводит имя, сервер сохраняет его и выводит на странице. Если значение попадает в HTML без экранирования, атакующий может передать фрагмент, который браузер воспримет как разметку или скрипт. Последовательность выглядит так: <code>fragment → render sink → document → script execution</code>. Здесь asset — страница профиля, а опасный побочный эффект — выполнение скрипта в контексте приложения.</p>\n<p>У каждого шага свой владелец. Валидатор и экранирование отвечают за смысл и безопасный вывод значения. CSP ограничивает разрешённые ресурсы и выполнение скриптов в документе. Браузер применяет политику, но не исправляет серверный шаблон. Поэтому ответ «в заголовке есть <code>Content-Security-Policy</code>» подтверждает доставку политики, а не безопасность каждого sink.</p>\n<figure><img src='/assets/editorial/2026/modern-web-security-2026-attack-control-evidence-map.svg' alt='Карта проверки веб-атаки: недоверенный фрагмент проходит к render sink и документу, CSP nonce прерывает выполнение, а источник фрагмента и непокрытые sink остаются остаточным риском' loading='lazy' /><figcaption>Карта связывает один маршрут с одной точкой прерывания и одним наблюдением. Остаточный риск не исчезает из-за положительного теста: источник фрагмента и другие sink нужно проверять отдельно.</figcaption></figure>\n<p>Для CSRF путь другой: внешняя страница создаёт запрос, браузер может приложить cookie, сервер принимает операцию записи. CORS управляет тем, сможет ли чужой скрипт прочитать ответ; он не является универсальным запретом на отправку запроса. Нельзя перенести evidence из одного маршрута на другой только потому, что оба проходят через браузер.</p>\n<h2>Разведите контроль и доказательство</h2>\n<p>Control полезно описывать глаголом: «экранирует значение перед вставкой», «отклоняет скрипт без разрешённого nonce», «отказывает в mutation без CSRF-токена», «не разрешает субъекту изменить чужой объект». Такая формулировка сразу показывает место проверки. Формула «CSP настроена» скрывает и переход, и ожидаемый результат.</p>\n<p>Evidence должно содержать вход, маршрут, наблюдение и границу вывода. Например: «на тестовом стенде страница с маркером <code>xss_probe</code> вернула CSP-заголовок; браузер заблокировал inline script без nonce; маркер не появился в журнале выполнения». Это подтверждает одну политику и один сценарий. Оно не доказывает безопасный вывод в другом шаблоне, отсутствие XSS во всём приложении или правильность конфигурации после прокси.</p>\n<div class='table-scroll'><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>Экранирование и безопасный sink</td><td>Недоверенное значение превращается в HTML или код</td><td>В DOM появляется текст, а не узел, который выполняет код</td><td>Другие шаблоны, sink и данные из другого источника</td></tr><tr><td>CSP с nonce</td><td>Скрипт без разрешённого nonce выполняется в документе</td><td>Браузер блокирует отрицательный сценарий и фиксирует нарушение политики</td><td>Что значение безопасно сформировали до вывода; поведение неподдерживаемого клиента</td></tr><tr><td>CSRF-токен</td><td>Чужой сайт отправляет state-changing запрос с cookie без доказательства намерения</td><td>Запрос без токена получает отказ, запись не меняется</td><td>XSS, права на чужой объект и маршруты без этого middleware</td></tr><tr><td>CORS</td><td>Чужой скрипт читает ответ cross-origin запроса</td><td>Браузер не отдаёт response body скрипту при запрещённом origin</td><td>Запрос без preflight не дошёл до сервера и состояние не изменилось</td></tr><tr><td>Авторизация</td><td>Субъект изменяет объект вне своей области прав</td><td>Запрос с валидной сессией к чужому объекту получает 403, запись неизменна</td><td>Наличие CSRF-защиты и безопасность клиентского кода</td></tr><tr><td><code>HttpOnly</code> / <code>SameSite</code></td><td>Чтение cookie скриптом или отправка cookie в части cross-site-контекстов</td><td>Фактические атрибуты cookie и запрос в проверяемом контексте</td><td>Полная защита от CSRF, XSS и украденной сессии</td></tr></tbody></table></div>\n<p>Таблица не заменяет тест-план. Она не говорит, что любой control обязателен для любого endpoint. Её задача — не дать одному зелёному наблюдению ответить сразу на пять разных вопросов.</p>\n<h2>Учебный пример: nonce не отменяет экранирование</h2>\n<p>Ниже — фрагмент в стиле Express для страницы, которая выводит имя. Он показывает порядок границ, но не является готовым middleware: <code>escapeHtml</code>, генерация nonce, шаблонизатор, заголовки прокси и обработка ошибок должны иметь реальные реализации и тесты. Важна последовательность: данные обезвреживаются до вывода, а политика передаётся браузеру вместе с документом.</p>\n<pre><code>import { randomBytes } from 'node:crypto';\n\napp.get('/profile', (req, res) =&gt; {\n const nonce = randomBytes(16).toString('base64');\n const safeName = escapeHtml(String(req.query.name ?? ''));\n\n res.set('Content-Security-Policy', [\n &quot;default-src 'self'&quot;,\n &quot;script-src 'nonce-&quot; + nonce + &quot;'&quot;,\n &quot;object-src 'none'&quot;,\n &quot;base-uri 'none'&quot;,\n ].join('; '));\n\n const html = '&lt;h1&gt;' + safeName + '&lt;/h1&gt;' +\n '&lt;script nonce=&quot;' + nonce + '&quot;&gt;' +\n 'window.profilePageReady = true;' +\n '&lt;/script&gt;';\n res.send(html);\n});</code></pre>\n<p>В этом коде nonce разрешает только явно помеченный скрипт текущего документа. Он не делает <code>safeName</code> безопасным: значение всё равно нужно корректно экранировать или передавать через безопасный API шаблонизатора. Если убрать экранирование, оставить только CSP и считать задачу решённой, доказательство будет неполным. W3C прямо описывает CSP как defence-in-depth для снижения последствий инъекции, а не замену проверке входа и безопасному выводу.</p>\n<p>Есть и практическое ограничение: пример не показывает, как фреймворк выставляет заголовок при ошибке, как политика проходит через CDN и как приложение обрабатывает inline-обработчики, сторонние скрипты и динамическую загрузку. Эти решения могут потребовать другие директивы и отдельные тесты. Нельзя добавлять <code>'unsafe-inline'</code> только ради того, чтобы старый тест перестал падать: это меняет саму границу, которую вы хотели проверить.</p>\n<h2>Воспроизводимая проверка на разрешённом стенде</h2>\n<p>Проверка должна отделять HTTP-наблюдение от поведения браузера. Команды ниже ничего не меняют: они скачивают страницу и заголовки с тестового URL. Подставьте адрес своего стенда, где разрешена проверка, а не production-сайта. Имя файла в примере — локальный временный артефакт ревью.</p>\n<pre><code>BASE_URL='https://staging.example.test'\nPAGE_PATH='/profile?name=xss_probe'\n\ncurl --fail-with-body -sS \\\n -D /tmp/profile.headers \\\n &quot;$BASE_URL$PAGE_PATH&quot; \\\n -o /tmp/profile.html\n\nrg -n -i '^content-security-policy:' /tmp/profile.headers\nrg -n 'xss_probe|nonce=' /tmp/profile.html\n\n# Ожидаем заголовок CSP и текстовый маркер.\n# Этот curl-запуск не доказывает, что браузер заблокировал код.\n! rg -n -i '&lt;script[^&gt;]*&gt;[^&lt;]*(xss_probe|alert)\\b' /tmp/profile.html</code></pre>\n<p>Для отрицательного сценария нужен браузерный тест. В него передают безопасный маркер, слушают событие нарушения CSP и проверяют, что код с неправильным nonce не создал наблюдаемого эффекта. Если тест запускается на странице с nonce, значение nonce нельзя зашивать в ожидание: сначала получите реальный документ, затем отдельно сформируйте намеренно неверный вариант. Иначе тест подтвердит только наличие атрибута, а не работу политики.</p>\n<pre><code>const violations = [];\npage.on('console', message =&gt; {\n if (message.type() === 'error') violations.push(message.text());\n});\n\nawait page.goto(baseUrl + '/profile?name=xss_probe');\nawait page.evaluate(() =&gt; {\n const script = document.createElement('script');\n script.textContent = 'window.__xssProbe = true';\n script.setAttribute('nonce', 'wrong-nonce');\n document.body.append(script);\n});\n\nconst executed = await page.evaluate(() =&gt; window.__xssProbe === true);\nif (executed) throw new Error('unexpected script execution');\nconsole.log({ executed, consoleErrors: violations.length });</code></pre>\n<p>Результат нужно записать точнее, чем «XSS закрыт»: «в браузере версии, использованной в тесте, скрипт с неверным nonce не выполнился; HTTP-ответ содержал ожидаемый CSP; безопасный вывод маркера проверен только на маршруте <code>/profile</code>». Если браузерный тест не поймал ошибку консоли, это ещё не повод ослаблять политику: консольные сообщения зависят от браузера. Надёжнее проверять отсутствие эффекта и, при необходимости, событие <code>securitypolicyviolation</code>.</p>\n<h2>Почему CORS, CSRF и права нельзя объединять в один verdict</h2>\n<p>Для cross-origin <code>fetch</code> браузер применяет CORS к чтению ответа. Некоторые запросы с безопасными для CORS методом, заголовками и типом содержимого не требуют preflight. Исторически HTML-форма и без этого механизма могла отправить запрос на другой origin, поэтому отсутствие ответа у чужого скрипта не означает отсутствие побочного эффекта на сервере. Если операция использует cookie и меняет состояние, серверу нужна отдельная CSRF-защита или эквивалентная проверка.</p>\n<p>CSRF-токен отвечает на другой вопрос: есть ли у запроса доказательство, которое внешний сайт не может получить из обычного cross-site сценария. Он не решает, имеет ли субъект право изменить объект. Для этого сервер сравнивает субъект сессии и владельца ресурса. <code>HttpOnly</code> мешает JavaScript прочитать cookie, но браузер всё равно может отправить её по правилам cookie. <code>SameSite</code> ограничивает часть отправок в cross-site-контексте, однако результат зависит от атрибута, браузера, схемы и типа навигации.</p>\n<p>Так же CSP не заменяет ни CSRF, ни авторизацию. Скрипт, который уже выполняется в доверенном контексте приложения, может вызвать разрешённую операцию; политика ресурсов не определяет права пользователя. Верный review не спрашивает «включена ли безопасность», а сопоставляет asset, угрозу, контроль, негативный тест и непроверенные соседние маршруты.</p>\n<h2>Порядок проверки</h2>\n<ol><li>Выберите один asset и один побочный эффект: например, имя в странице профиля или изменение email.</li><li>Опишите attack path глаголами: откуда приходит значение, через какой sink проходит, где появляется документ и какое действие возможно.</li><li>Назначьте один control на один переход. Для CSP это выполнение запрещённого скрипта, для CSRF — запись без токена, для авторизации — доступ к чужому объекту.</li><li>Запишите позитивный и негативный сценарии. Позитивный показывает разрешённый контракт, негативный должен доходить до точки отказа и не менять состояние.</li><li>Проверьте HTTP отдельно: статус, заголовок, cookie-атрибуты и тело ответа. Не выдавайте отсутствие response body за отсутствие серверного эффекта.</li><li>Проверьте браузер отдельно: реальное применение CSP, выполнение скрипта, preflight и отправка credentials зависят от контекста и реализации клиента.</li><li>Сверьте покрытие: найдите соседние шаблоны, mutation, формы, фоновые обработчики и прокси, которые не участвовали в тесте.</li><li>Передайте результат четырьмя полями: asset, interruption, evidence и not-proven. Если одного поля нет, оставьте следующий шаг, а не общий verdict.</li></ol>\n<h2>Ограничения применимости</h2>\n<p>Это метод локальной проверки одного маршрута, а не сертификат безопасности приложения. Он не покрывает автоматически WebSocket, GraphQL, загрузку файлов, OAuth callback, Service Worker, фоновые очереди и native-клиенты. Для каждого канала нужно построить собственный путь и назвать границу, которая действительно применяется.</p>\n<p>Nonce и CSP зависят от того, как формируется документ и какие скрипты нужны странице. Политика из примера может сломать легитимную аналитику или inline-код. Не переносите её в другой сервис без инвентаризации ресурсов. <code>curl</code> не исполняет HTML и потому не подтверждает браузерную защиту. Браузерный тест не доказывает, что сервер безопасно выводит все данные. Тест на тестовом стенде не доказывает конфигурацию после deploy.</p>\n<p>CSRF-токен не защищает от украденной сессии или скрипта, который уже выполняется в доверенном origin. CORS не является заменой контролю записи. <code>HttpOnly</code> и <code>SameSite</code> уменьшают отдельные риски, но не являются универсальной защитой. Если команда не может назвать непроверенный sink, маршрут или контекст cookie, карта слишком широкая для честного вывода.</p>\n<h2>Критерий готовности</h2>\n<p>Для выбранной операции готово не «всё защищено», а более узкое утверждение: путь атаки записан; control стоит до опасного эффекта; позитивный сценарий работает; негативный сценарий получает ожидаемый отказ и не меняет состояние; HTTP- и браузерное наблюдения разделены; список непроверенных маршрутов сохранён. После изменения шаблона, middleware, cookie-политики, прокси или браузера этот набор нужно повторить.</p>\n<p>Такой критерий даёт команде полезный результат: видно, что остановлено, где осталось residual risk и кто должен выполнить следующий review. Если evidence подтверждает только заголовок, пишите только про заголовок. Если тест подтверждает один sink, не называйте его защитой всего приложения.</p>\n<h2>Проверяемые источники</h2><ul><li><a href='https://www.w3.org/TR/2026/WD-CSP3-20260421/' target='_blank' rel='noopener noreferrer'>W3C: Content Security Policy Level 3, Working Draft от 21 апреля 2026 года</a> — определяет CSP и прямо ограничивает её роль defence-in-depth: политика снижает риск и последствия content injection, но не заменяет проверку входа и безопасное кодирование вывода. Это спецификация, а не доказательство конфигурации конкретного сайта.</li><li><a href='https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS' target='_blank' rel='noopener noreferrer'>MDN: Cross-Origin Resource Sharing (CORS)</a> — описывает CORS, preflight, credentials и то, что простые запросы могут не проходить preflight. Материал объясняет поведение браузера, но не проверяет серверный побочный эффект.</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, double-submit cookie и дополнительным проверкам origin. Рекомендация не является результатом теста конкретного endpoint.</li><li><a href='https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie' target='_blank' rel='noopener noreferrer'>MDN: Set-Cookie</a> — фиксирует назначение атрибутов <code>HttpOnly</code> и <code>SameSite</code> и их влияние на доступ скрипта и отправку cookie. Фактический результат нужно проверять в целевом браузерном контексте.</li></ul>"
}