Files

8 lines
21 KiB
JSON
Raw Permalink 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": 3,
"slug": "editorial-2027-12-practice-author-manifesto",
"title": "Security headers: CSP и HSTS без иллюзии защиты",
"excerpt": "Как CSP и HSTS ограничивают браузер, почему nonce и HTTPS не заменяют исправление XSS и как вводить политики с проверяемым откатом.",
"contentHtml": "<p>Сбой обычно замечают в двух местах: DevTools показывает нарушение Content Security Policy (CSP), а переход на <code>http://</code> сначала попадает на редирект вместо немедленной замены схемы. Если в этот момент просто добавить длинную строку заголовков, приложение может продолжить выполнять инъецированный код, а первый HTTP-запрос всё ещё может уйти без шифрования. Цена ошибки — украденная сессия, отправленная форма или недоступный поддомен, а не «потерянный зелёный чек».</p>\n<p>Главный вопрос не в том, какой шаблон заголовка скопировать, а в том, какую границу он действительно проводит. CSP ограничивает ресурсы и выполнение внутри страницы. HSTS учит браузер обращаться к уже известному хосту только по HTTPS. Ни один из них не исправляет XSS, не выдаёт сертификат и не делает безопасным секрет, который уже попал в JavaScript. Значит, внедрение нужно вести как проверяемое изменение поведения браузера: симптом → причина → отрицательный тест → откат.</p>\n<h2>Две политики, два владельца</h2>\n<p>CSP приходит в ответе страницы и применяется к её документу или связанному worker-контексту. Браузер сопоставляет попытку загрузки или выполнения с директивой: <code>script-src</code> отвечает за JavaScript, <code>connect-src</code> — за fetch/XHR и другие соединения, <code>img-src</code> — за изображения, а <code>default-src</code> задаёт запасное правило там, где нет более узкой директивы. <code>object-src 'none'</code> закрывает plugin-контент, если он приложению не нужен; <code>base-uri 'self'</code> ограничивает адреса, допустимые в элементе <code>base</code>.</p>\n<p>HSTS работает иначе. После корректного заголовка <code>Strict-Transport-Security</code>, полученного по TLS без ошибки, браузер сохраняет хост как известный HSTS-хост на срок <code>max-age</code>. При следующем обращении к нему по HTTP браузер сам меняет схему на HTTPS. Заголовок, пришедший по обычному HTTP, браузер обязан проигнорировать. Поэтому серверный redirect нужен для первого контакта и старых клиентов, но не заменяет HSTS для клиента, который ещё не знает домен.</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>CSP</td><td>Блокирует ресурс или выполнение, не совпавшее с политикой; может только отправить отчёт</td><td>Не санитизирует HTML и не исправляет небезопасный <code>innerHTML</code></td><td>Негативные сценарии страницы; владелец приложения</td></tr><tr><td>HSTS</td><td>Для известного хоста заменяет HTTP на HTTPS и требует успешный TLS</td><td>Не защищает первый HTTP-переход и не чинит сертификаты</td><td>HTTPS-ответы и каждый поддомен; владелец платформы</td></tr><tr><td>Redirect</td><td>Получает ответ сервера с новой схемой</td><td>Не скрывает исходный HTTP-запрос от сетевого посредника</td><td>Цепочка 3xx и канонический URL; владелец edge</td></tr><tr><td>Исправление XSS</td><td>Убирает исполняемый ввод из приложения</td><td>Не заменяется заголовком</td><td>Контекстное экранирование и безопасные API; владелец кода</td></tr></tbody></table>\n<figure><img src='/assets/editorial/2027/author-manifesto-2027-editorial-decision-graph.svg' alt='Схема двух независимых слоёв: карта ресурсов ведёт к CSP, HTTPS-ответ ведёт к HSTS, неизвестный ресурс блокируется или попадает в отчёт' loading='lazy' /><figcaption>Карта ресурсов и HTTPS-ответ входят в разные ветки. Их нельзя свести к одной «строке безопасности».</figcaption></figure>\n<h2>Как CSP создаёт барьер, а не иллюзию</h2>\n<p>Начните с одной страницы и составьте список фактических источников: скрипты, inline-блоки, стили, шрифты, изображения, API, iframe, service worker и plugin-контент. Затем выражайте этот список в директивах. Разрешение <code>https:</code> или широкого CDN может убрать сообщения в консоли, но одновременно разрешить больше серверов, чем нужно странице. Узкий список — не самоцель: каждую внешнюю зависимость нужно связать с конкретной загрузкой и владельцем.</p>\n<p><code>Content-Security-Policy-Report-Only</code> позволяет сначала наблюдать нарушения без блокировки. Это полезный этап инвентаризации, но не защита: сломанный или вредоносный inline-скрипт продолжит выполняться. После разбора отчётов ту же политику переводят в <code>Content-Security-Policy</code>. Проверка должна включать отрицательный путь — например, попытку загрузить <code>https://unknown.example/evil.js</code> — иначе успешная загрузка штатного bundle ничего не доказывает.</p>\n<p>Nonce — исключение для конкретного inline-скрипта. Сервер создаёт новое случайное значение для каждого ответа, помещает его в <code>script-src</code> и в атрибут <code>nonce</code> нужного элемента. CSP Level 3 требует уникальное значение; спецификация рекомендует не менее 128 бит до кодирования и криптографически стойкий генератор. Статическая строка в конфигурации нарушает эту модель: её можно повторно использовать и предсказать.</p>\n<p>Nonce разрешает inline-скрипт, но не inline-обработчик события вроде <code>onclick</code> и не произвольный HTML. Если пользовательская строка попала в DOM через небезопасный sink, атакующий может использовать разрешённый bootstrap или другой разрешённый путь. Поэтому исправление XSS, контекстное экранирование и безопасные DOM-API остаются первым слоем, а CSP — ограничителем последствий.</p>\n<h2>Самодостаточный пример заголовков</h2>\n<p>Ниже — учебный файл <code>security-headers.mjs</code>. Он запускается в Node.js без сторонних пакетов, создаёт nonce и проверяет связь между политикой и HTML. Значения <code>img-src</code>, <code>connect-src</code>, <code>frame-ancestors</code> и годовой <code>max-age</code> — проектный пример, а не готовая политика для любого сайта.</p>\n<pre><code>import { randomBytes } from 'node:crypto';\n\nfunction buildSecurityHeaders() {\n const nonce = randomBytes(16).toString('base64');\n const contentSecurityPolicy = [\n \"default-src 'self'\",\n \"base-uri 'self'\",\n \"object-src 'none'\",\n \"script-src 'self' 'nonce-\" + nonce + \"'\",\n \"style-src 'self'\",\n \"img-src 'self' data:\",\n \"connect-src 'self'\",\n \"frame-ancestors 'none'\",\n \"form-action 'self'\",\n ].join('; ');\n\n return {\n nonce,\n headers: {\n 'Content-Security-Policy': contentSecurityPolicy,\n 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',\n },\n };\n}\n\nconst { nonce, headers } = buildSecurityHeaders();\nconst bootstrap = \"&lt;script nonce='\" + nonce + \"'&gt;startApplication();&lt;/script&gt;\";\nconst csp = headers['Content-Security-Policy'];\nconst nonceSource = \"'nonce-\" + nonce + \"'\";\n\nif (!csp.includes(nonceSource)) throw new Error('nonce is absent from CSP');\nif (!bootstrap.includes(\"nonce='\" + nonce + \"'\")) throw new Error('nonce is absent from HTML');\nif (csp.includes(\"'unsafe-inline'\")) throw new Error('broad inline permission detected');\n\nconsole.log({ headers, bootstrap });</code></pre>\n<p>Запуск <code>node security-headers.mjs</code> должен вывести два заголовка и один bootstrap с одинаковым nonce. Это проверяет только сборку значения. Серверный адаптер всё ещё обязан отправить заголовки именно в HTTPS-ответе HTML и передать тот же nonce шаблону. При кэшировании страницы нужно проверить, что HTML и CSP не разъезжаются: сохранённый HTML со старым nonce при новом заголовке будет заблокирован, а повторно использованный nonce ослабит модель.</p>\n<p>Для страницы с внешним CDN добавьте конкретный origin только после проверки реального запроса, например <code>https://cdn.example/asset.js</code>, и повторите отрицательный тест. Не добавляйте <code>'unsafe-inline'</code> или <code>'unsafe-eval'</code> ради исчезновения одной ошибки сборки: сначала выясните, какой код создаёт inline или динамическое выполнение. Если библиотека требует это исключение, запишите риск, владельца и срок удаления рядом с конфигурацией.</p>\n<h2>HSTS: что происходит до и после первого посещения</h2>\n<p>У HSTS есть неудобная граница: политика появляется только после доверенного HTTPS-ответа, если браузер заранее не знает домен из своего preload-списка. Пользователь, впервые открывший <code>http://example.test</code>, сначала зависит от сети и server redirect. Посредник может изменить этот первый запрос или ответ до того, как браузер узнает о HSTS. Поэтому порядок такой: исправить TLS, включить редирект на HTTPS, проверить сертификат и только затем отдать HSTS в HTTPS-ответе.</p>\n<p><code>includeSubDomains</code> расширяет политику на все поддомены. Это не декоративный флаг: отдельный API, старый кабинет, health endpoint или внешний инструмент может перестать открываться, если он не готов к HTTPS или имеет другой сертификат. Сначала соберите список имён и владельцев, затем проверьте их вручную и автоматикой. В учебном примере выше включение флага — осознанное значение для домена, где такая инвентаризация уже пройдена.</p>\n<p><code>max-age=0</code> позволяет сообщить браузеру по HTTPS, что политика хоста больше не должна считаться действующей. Это не мгновенный глобальный откат: клиент должен снова успешно соединиться по HTTPS, а другие клиенты могли ещё не получить новое значение. Чем дольше срок и шире область, тем дороже ошибка конфигурации. HSTS нельзя использовать как замену управляемому rollout и плану восстановления.</p>\n<h2>Runbook перед переводом в enforce</h2>\n<ol><li><strong>Зафиксировать границы.</strong> Выпишите страницу, её домен, поддомены, типы ресурсов и владельцев. Отдельно отметьте inline-код, динамический <code>eval</code>, iframe, worker, CDN и кэш.</li><li><strong>Проверить TLS и redirect.</strong> Выполните <code>curl -sS -D - -o /dev/null https://example.test/</code> и убедитесь, что HTTPS-ответ содержит ровно одну CSP и один HSTS. Для <code>http://</code> проверьте код и <code>Location</code>; не принимайте redirect за доказательство HSTS.</li><li><strong>Собрать нарушения.</strong> Отправьте CSP в Report-Only на одной странице, откройте штатные сценарии и сгруппируйте сообщения по директиве, URL и типу ресурса. Фильтруйте шум, но не удаляйте повторяемые нарушения без объяснения.</li><li><strong>Сузить политику.</strong> Вынесите inline-код в файл или выдайте ему свежий nonce. Удалите широкие источники и лишние исключения. Для каждого оставшегося origin укажите запрос, владельца и тест.</li><li><strong>Проверить блокировку.</strong> Переведите CSP в enforce на тестовом маршруте. Убедитесь, что штатный сценарий работает, неизвестный script блокируется, запрещённый frame не встраивает страницу, а форма не уходит на непредусмотренный origin.</li><li><strong>Проверить поддомены.</strong> Для каждого имени под <code>includeSubDomains</code> проверьте сертификат, HTTPS-ответ, API-клиента и административные пути. Если список неполон, начните с области без флага и не расширяйте её по привычке.</li><li><strong>Закрепить контроль.</strong> Добавьте в CI проверку обязательных директив и отсутствия случайных <code>unsafe-inline</code>/<code>unsafe-eval</code>. В релизном плане укажите владельца, наблюдаемость нарушений и восстановление через версионируемую конфигурацию.</li></ol>\n<h2>Ограничения и критерий готовности</h2>\n<p>Эта статья не обещает, что два заголовка закрывают всю модель угроз. CSP зависит от того, какие контексты и браузеры поддерживает продукт, как CDN переписывает ответы, где рендерится HTML и какие сервисы загружаются после навигации. Worker, iframe, кэш, service worker, API CORS и сертификаты нужно проверять отдельно. В частности, HSTS не заменяет настройку TLS, а CSP не контролирует логику сервера или уже украденный токен.</p>\n<p>В указанной датированной редакции W3C CSP Level 3 имеет статус Working Draft, поэтому директиву, поддержку браузеров и поведение конкретной версии следует сверять перед релизом. RFC 6797 — нормативная спецификация HSTS, но она не превращает первый HTTP-переход в защищённый. Если проект использует preload-список, рассматривайте его как отдельный операционный процесс с собственными условиями и стоимостью вывода домена.</p>\n<p>Готовность можно доказать коротким пакетом: HTML-ответ содержит согласованные CSP и HSTS по HTTPS; Report-Only нарушения разобраны; enforce блокирует искусственно добавленный неизвестный ресурс; штатный bootstrap проходит с новым nonce; каждый поддомен прошёл TLS-проверку; CI ловит ослабление политики. Если хотя бы один пункт неизвестен, безопаснее оставить конкретную границу в ограничении и назначить следующий тест, чем назвать заголовок защитой.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://www.w3.org/TR/2026/WD-CSP3-20260813/\" target='_blank' rel='noopener noreferrer'>W3C Content Security Policy Level 3, редакция от 13 августа 2026 года</a> — описание директив, enforce и Report-Only; разделы о nonce требуют уникальное значение и рекомендуют не менее 128 бит до кодирования. Статус документа — Working Draft, поэтому поддержку целевых браузеров нужно проверять отдельно.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc6797.html\" target='_blank' rel='noopener noreferrer'>RFC 6797 — HTTP Strict Transport Security</a> — Standards Track; описывает получение HSTS по защищённому транспорту, <code>max-age</code>, <code>includeSubDomains</code> и обязательное игнорирование STS-заголовка, пришедшего по незашифрованному соединению.</li></ul>"
}