Большая часть безопасности сайта живёт в его коде, но небольшой набор заголовков ответа HTTP решает, что браузер согласится делать с этим кодом, когда тот до него доберётся. Это разница между внедрённым <script>, который исполняется, и тем, который браузер отказывается запускать; между первым визитом по незашифрованному HTTP и визитом, который поднимается до HTTPS ещё до того, как покинет адресную строку.
К тому же их необычно дешево внедрять. Заголовок — это конфигурация, а не код: большинство из них укладывается в одну строку на вашем веб-сервере, в CDN или фреймворке и действует сразу на все ответы. В руководстве разобрано, что делает каждый заголовок, какие из них себя оправдывают, как внедрить Content-Security-Policy, не сломав собственный сайт, и как проверить, что вы отправляете сегодня.
Почему заголовки безопасности важны
Заголовок — это инструкция браузеру, а браузер — единственная сторона, способная выполнить её на машине пользователя. Это делает заголовки второй линией обороны, которая держится, даже когда выше по течению уже что-то пошло не так:
- Они сдерживают уже допущенные ошибки. Content-Security-Policy не закрывает дыру XSS, но может не дать внедрённому скрипту запуститься или что-либо отправить обратно.
- Они закрывают целые классы атак. Кликджекинг, путаница MIME-типов и откат на HTTP — не ошибки в вашем коде, а поведение браузера, от которого нужно явно отказаться.
- Они останавливают тихие утечки данных. Без политики реферера полный URL приватной страницы уезжает к каждой третьей стороне, с которой вы подгружаете картинку.
- Это первое, на что смотрит аудитор. Отсутствующие заголовки всплывают в каждом автоматическом сканировании, опроснике по безопасности и проверке поставщика, потому что это самая дешёвая из возможных находок.
Загвоздка в том, что их легко сделать почти правильно. CSP с unsafe-inline блокирует очень мало; заголовок HSTS со сроком в один день почти никого не защищает. Вся ценность — в деталях.
Заголовки, которые важнее всего
Шесть заголовков делают почти всю полезную работу. Остальные либо унаследованы, либо крайне узки, либо вытеснены.
| Заголовок | Что предотвращает | Трудозатраты |
|---|---|---|
Content-Security-Policy | XSS, выгрузку данных, неожиданный сторонний код | Высокие |
Strict-Transport-Security | Откат на HTTP, кражу cookie при первом запросе | Низкие |
X-Content-Type-Options | Исполнение загруженного файла как скрипта | Минимальные |
Referrer-Policy | Утечку полных URL третьим сторонам | Минимальные |
Permissions-Policy | Доступ к камере, микрофону и геолокации | Низкие |
Cross-Origin-Opener-Policy | Атаки через окна со страниц, которые вы открываете | Низкие |
Начните снизу. Четыре дешёвых заголовка — по одной строке каждый, и они почти никогда ничего не ломают, так что их стоит отправить уже сегодня; плана требует именно CSP.
Content-Security-Policy
CSP — это список разрешённого, сообщающий браузеру, откуда может приходить каждый вид ресурсов. Достаточно строгая политика выглядит так:
default-src 'self'; script-src 'self' 'nonce-r4nd0m'; style-src 'self'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; object-src 'none'
Читайте директиву за директивой. default-src 'self' — запасной вариант: всё, что не названо явно, может загружаться только с вашего собственного источника. script-src важнее всего, потому что именно скрипты злоумышленник хочет запустить. frame-ancestors 'none' запрещает любому другому сайту помещать вашу страницу в iframe и является современной заменой X-Frame-Options. object-src 'none' и base-uri 'self' закрывают два старых обходных пути, перекрыть которые ничего не стоит.
Два значения решают, настоящая ли ваша политика. 'unsafe-inline' в script-src разрешает любой встроенный <script>, включая внедрённый злоумышленником, — то есть ровно то, ради предотвращения чего CSP и существует. 'unsafe-eval' разрешает eval() и его родственников. Политика с любым из них всё ещё полезна для нескриптовых директив, но от XSS она вас не защищает.
Выход — nonce или хеш. Nonce — случайное значение, генерируемое на каждый ответ и прикрепляемое и к заголовку, и к каждому легитимному <script nonce="...">; браузер запускает только скрипты с этим значением, а внедрённый скрипт его не угадает. Хеши работают так же для скриптов, которые никогда не меняются. Если ваш фреймворк отдаёт встроенные скрипты, почти наверняка поддержка nonce в нём уже есть — обычно её достаточно включить в настройках.
Strict-Transport-Security
HSTS предписывает браузеру отказывать в незашифрованном HTTP для вашего домена на заданный срок, и это закрывает щель, которую один редирект оставляет открытой: самый первый запрос, до того как редирект доедет, идёт в открытом виде и может быть перехвачен.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
max-age задаётся в секундах; обычная цель — год, а меньше нескольких месяцев даёт мало. includeSubDomains распространяет правило на все поддомены — именно это делает его действенным и именно это стоит проверить первым: любой поддомен, который всё ещё отдаётся по HTTP, перестанет работать. preload запрашивает включение в список, который браузеры поставляют с собой, так что защищён даже первый визит.
Считайте HSTS трудно обратимым. Браузер, увидевший годовую политику, будет применять её год, что бы вы ни отправили потом. Внедряйте с коротким max-age, убедитесь, что ничего не сломалось, и только затем увеличивайте. И поскольку HSTS имеет смысл лишь при действительном сертификате, проверьте сертификат и его срок с помощью проверки SSL-сертификата, прежде чем брать на себя долгую политику.
Небольшие заголовки
Эти четыре — по одной строке каждый и редко вызывают споры:
X-Content-Type-Options: nosniff— не даёт браузеру угадывать тип ответа по его содержимому. Без него файл, загруженный как.txt, может быть исполнен как скрипт, если браузер решит, что он на скрипт похож.Referrer-Policy: strict-origin-when-cross-origin— отправляет полный URL на ваш собственный источник, только источник — на другие HTTPS-сайты и ничего — при переходе на HTTP. Это современное значение по умолчанию, и оно не даёт приватным путям и параметрам утекать третьим сторонам.Permissions-Policy: camera=(), microphone=(), geolocation=()— отключает возможности устройства, которыми ваш сайт не пользуется, и для вашей страницы, и для всего, что вы встраиваете.Cross-Origin-Opener-Policy: same-origin— разрывает оконную связь со страницами, которые вы открываете или которые открывают вас, что блокирует целое семейство атак через окна и является предварительным условием для некоторых механизмов изоляции в браузере.
X-Frame-Options: DENY всё ещё уместен для старых браузеров, но frame-ancestors в вашей CSP заменяет его и выразительнее. Отправляйте оба: они не конфликтуют.
Как безопасно внедрить CSP
CSP, написанная наугад, заблокирует что-то нужное вашему же сайту. Путь «сначала отчёты» избавляет от этого полностью:
- Отправьте четыре дешёвых заголовка сегодня.
nosniff, политику реферера, политику разрешений и HSTS с короткимmax-age. Ни один из них обычно не ломает работающий сайт. - Добавьте
Content-Security-Policy-Report-Only. Синтаксис тот же, но браузер только сообщает о нарушениях, а не блокирует их. Сайт ведёт себя точно как прежде. - Собирайте отчёты одну-две недели. Они назовут каждый сторонний скрипт, шрифт, конечную точку аналитики и встроенный обработчик, о которых вы забыли. Этот шаг чаще всего пропускают, и именно он делает всё остальное безболезненным.
- Затягивайте, пока отчёты не стихнут. Добавьте источники, которые вам действительно нужны, и уберите те, в которых, как выяснилось, нужды нет. Не расширяйте директиву до
*, чтобы заглушить один отчёт. - Переключитесь на применение. Переименуйте заголовок в
Content-Security-Policy. Оставьте рядом вариант report-only для будущей проверки более строгой политики. - Уберите
unsafe-inlineпоследним. Это и есть настоящая работа: перенести встроенные скрипты в файлы или выдать им nonce. Всё предыдущее — подготовка; именно этот шаг останавливает XSS.
Практическое замечание: заголовки действуют на каждый ответ, поэтому задавайте их на границе — в CDN, обратном прокси или промежуточном слое фреймворка, — а не страница за страницей. Задать их в одном месте — ещё и единственный способ убедиться, что их несут и страницы ошибок, и редиректы, а именно там их обычно и не хватает.
Заголовки, которые можно больше не отправлять
Часть советов плохо постарела, а отправлять мёртвый заголовок не безвредно: он добавляет байты и создаёт впечатление завершённого аудита там, где он не завершён.
X-XSS-Protection— фильтров браузера, которыми он управлял, больше нет. Ни в одном современном браузере он ничего не делает, а старое значениеmode=blockкогда-то создавало собственные проблемы. Уберите его и опирайтесь на CSP.Public-Key-Pins— HPKP удалён из браузеров. Было слишком легко закрыть себе доступ к собственному домену, поэтому от него отказались.Expect-CT— прозрачность сертификатов теперь обеспечивается по умолчанию, так что заголовок избыточен.Feature-Policy— переименован вPermissions-Policyс другим синтаксисом. Отправляйте новый; старый оставляйте только если нужно поддерживать что-то очень древнее.
Напротив, кое-что стоит убрать — сведения, которые вы выдаёте сами: заголовок Server или X-Powered-By с точной версией вашего фреймворка подсказывает злоумышленнику, какие эксплойты пробовать первыми. Это не уязвимость, но и публиковать это незачем.
Как проверить свои заголовки
Заголовки публичны, так что проверить настройку любого сайта можно за секунды:
curl -I https://example.com. ЗапросHEADпечатает все заголовки ответа, не загружая страницу. Добавьте-L, чтобы смотреть конечный URL, а не редирект.- Панель «Сеть» в браузере. Откройте инструменты разработчика, перезагрузите страницу, выберите основной документ и прочитайте раздел заголовков ответа. Консоль к тому же записывает каждое нарушение CSP вместе с заблокировавшей директивой — это самый быстрый способ отладить политику.
- Проверка с оценкой результата. Наш инструмент проверки заголовков безопасности запрашивает URL и показывает, какие заголовки есть, каких не хватает и где имеющийся заголовок слабее, чем кажется.
Проверяйте не только главную. Заголовки часто выставлены на основном приложении, но не на хостах статики, страницах ошибок, поддоменах API или на редиректе с www на основной домен. Заодно убедитесь, что и сама цепочка редиректов верна; справочник кодов состояния HTTP объясняет, какой код подходит для постоянного переезда.
Чтобы составить политику, не заучивая синтаксис, начните с нашего генератора CSP — он также отмечает настройки, которые её ослабляют.
Типичные ошибки
- CSP с
unsafe-inlineвscript-src. Она проходит большинство автоматических проверок и почти не останавливает XSS. Используйте nonce или хеши. - Расширение директивы до
*ради одного отчёта. Добавьте конкретный источник;*отказывается от директивы целиком. - HSTS с долгим
max-ageв первый же день. На опубликованный срок это практически необратимо. Начните с короткого. includeSubDomainsдо проверки поддоменов. Всё, что осталось на HTTP, включая внутренние инструменты, перестаёт загружаться.- Заголовки в приложении, но не на границе. Страницы ошибок, редиректы и хосты статики остаются без защиты, а именно к этим ответам и тянутся злоумышленники.
- Один и тот же заголовок дважды. Два заголовка CSP пересекаются, а не объединяются, поэтому результат строже каждого из них и обычно что-то ломает; два противоречащих значения
X-Frame-Optionsмогут быть проигнорированы целиком. - Всё ещё отправлять
X-XSS-Protectionи считать дело сделанным. Мёртвый заголовок даёт ложное спокойствие в отчёте. - Никогда не выходить из режима report-only. Отчёты, по которым вы не действуете, никого не защищают.
Проверьте свои заголовки
Заголовки безопасности дают лучшее соотношение защиты к усилиям, какое есть в интернете: четыре из них — по одной строке, а пятый, CSP, сводится в основном к тому, чтобы выслушать собственные отчёты о нарушениях прежде, чем что-либо применять. Начните с того, что ваш сайт отправляет сегодня, добавьте дешёвые заголовки сразу, а затем включите CSP в режиме report-only и дайте ей рассказать, что должна содержать ваша политика.
Связанные инструменты и руководства:
- Проверка заголовков безопасности — посмотрите, какие заголовки отправляет URL и каких не хватает.
- Проверка SSL-сертификата — проверьте сертификат за вашей политикой HSTS.
- Коды состояния HTTP — убедитесь, что ваш редирект на HTTPS возвращает верный код.
- DNS-запрос: полное руководство — записи за каждым именем хоста в вашей политике.
Частые вопросы
Какие заголовки безопасности добавить первыми?⌄
Начните с четырёх, которые почти никогда ничего не ломают: X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, Permissions-Policy, отключающая неиспользуемые возможности устройства, и Strict-Transport-Security с коротким max-age, который вы потом увеличите. Затем добавьте Content-Security-Policy в режиме report-only, потому что именно она требует наблюдения до применения.
Помогает ли CSP с unsafe-inline?⌄
Помогает во всём, кроме той атаки, которую CSP останавливает лучше всего. Директивы img-src, connect-src, frame-ancestors и object-src по-прежнему ограничивают, куда могут уйти данные и кто может поместить вас в рамку. Но 'unsafe-inline' в script-src разрешает запуск любого внедрённого встроенного скрипта, так что от XSS политика не защищает. Замените его на nonce для каждого ответа или на хеши скриптов.
Опасен ли Strict-Transport-Security?⌄
Опасен прежде всего тем, что его трудно отменить: браузер, увидевший max-age=31536000, будет отказывать в незашифрованном HTTP для вашего домена год, что бы вы ни отправили дальше. Внедрите сначала с коротким max-age, убедитесь, что каждый поддомен доступен по HTTPS, прежде чем добавлять includeSubDomains, и только потом увеличивайте срок и думайте о preload.
Что заменило X-Frame-Options и X-XSS-Protection?⌄
Директива frame-ancestors в Content-Security-Policy заменяет X-Frame-Options и выразительнее, хотя отправлять оба безвредно и до сих пор помогает старым браузерам. У X-XSS-Protection замены нет, потому что фильтры браузера, которыми он управлял, были удалены; XSS теперь останавливает CSP, а старый заголовок можно просто убрать.
Где следует задавать заголовки безопасности?⌄
На границе, в одном месте: в CDN, обратном прокси или промежуточном слое фреймворка. Если задавать их постранично, без прикрытия останутся страницы ошибок, редиректы, хосты статики и ответы API — именно те ответы, что упускают при аудите и выбирают при атаке.
Как узнать, какие заголовки отправляет мой сайт?⌄
Запустите curl -I https://ваш-домен.ru, добавив -L, чтобы увидеть конечный адрес, а не редирект, либо откройте панель «Сеть» в браузере и прочитайте заголовки ответа для основного документа. Инструмент проверки заголовков безопасности делает это из браузера и отмечает как отсутствующие заголовки, так и присутствующие, которые слабее, чем выглядят.
Более 15 лет практического опыта в веб-разработке и безопасности. Специализация — практическая веб-безопасность: чтобы сайты были защищёнными, надёжными и легко диагностировались. BuildToolForge создан, чтобы такие проверки были бесплатными, приватными и понятными. Инструменты по возможности работают прямо в браузере, а всё, что вы вводите в инструмент, не сохраняется.
Как мы пишем руководства: Каждое руководство основано на практическом опыте, сверено с официальными стандартами и проверено нашими инструментами на реальных доменах. Мы обновляем руководства, когда меняются стандарты или инструменты. Нашли ошибку? Напишите нам: contact@buildtoolforge.com.