8 lines
21 KiB
JSON
8 lines
21 KiB
JSON
{
|
||
"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 = \"<script nonce='\" + nonce + \"'>startApplication();</script>\";\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>"
|
||
}
|