Заголовки безопасности: что они делают и почему не работают «из коробки»
Заголовки безопасности — это HTTP-заголовки, которые браузеры используют для изменения поведения страницы. Они появились как защита от уязвимостей, последствия которых разыгрываются в браузере: межсайтового скриптинга, кликджекинга, атак через загрузку ресурсов с чужих доменов. От кликджекинга заголовок и есть основная защита, а XSS закрывается в первую очередь в коде, экранированием при выводе, и заголовок лишь сужает последствия ошибки. Но не все заголовки одинаково полезны: одни действительно меняют поведение браузера, другие остались от прошлых версий спецификаций и ничего не делают.
Начнём с того, что заголовки безопасности — это не серебряная пуля. Они не заменяют проверку ввода на сервере, не защищают от утечек данных через сторонние сервисы и не устраняют уязвимости в коде. Но они могут усложнить жизнь злоумышленникам, если их правильно настроить.
Тупик: почему просто добавить заголовки недостаточно
Первое, что обычно пробуют — взять список рекомендованных заголовков и добавить их на сайт. Кажется логичным: если эти заголовки защищают, то чем больше их добавить, тем лучше. Но на практике это приводит к проблемам:
-
Сайт ломается. Например, если добавить CSP в режиме запрета без предварительной настройки, перестанут работать встроенные скрипты, виджеты, аналитика. Пользователи увидят пустую страницу или неработающие элементы интерфейса.
-
Заголовки конфликтуют друг с другом. Например,
X-Frame-Options: DENYи CSP сframe-ancestors 'self'говорят разное, и результат будет зависеть от браузера. Первый запрещает встраивание страницы вообще, второй разрешает его на своём домене. Браузер, понимающийframe-ancestors, применит её и проигнорируетX-Frame-Options, а старый браузер — наоборот, и страница поведёт себя в них по-разному. -
Заголовки не работают из-за неверных настроек. Например, со сроком
max-ageу HSTS ошибаются в обе стороны. Слишком короткий срок оставляет окно: когда он истёк, первый запрос снова уходит по HTTP и может быть перехвачен. Слишком длинный запирает сайт на HTTPS: если сертификат сломается или понадобится вернуть HTTP, браузеры посетителей будут требовать HTTPS до конца срока, а со стороны сайта снять правило можно только ответомmax-age=0по работающему HTTPS. -
Заголовки не защищают от реальных атак. Например, CSP не устраняет саму дыру. Если данные попадают в разметку без экранирования, строгая политика мешает выполнить внедрённый скрипт, но не мешает внедрить разметку. А политика, которая разрешает встроенные скрипты (
'unsafe-inline'без nonce или хеша), от внедрённого скрипта не защищает.
Поэтому просто добавить заголовки недостаточно. Их нужно настраивать под конкретный сайт, проверять и исправлять нарушения.
Какие заголовки действительно работают
Content-Security-Policy (CSP)
CSP — это политика содержимого, которая ограничивает источники загрузки ресурсов: скриптов, стилей, изображений, шрифтов. Она нужна, чтобы ограничить последствия межсайтового скриптинга (XSS): браузер выполняет только скрипты из разрешённых источников и не выполняет встроенный в разметку скрипт или обработчик события, если они явно не разрешены. Поэтому встроенный скрипт или обработчик события, внедрённый в страницу через ошибку, не запустится, пока политика сама не разрешает встроенные скрипты. А вот внедрённый тег, который подключает скрипт с разрешённого источника, выполнится. Если на разрешённом домене лежит код, пригодный атакующему, — общий CDN с библиотеками, файлы, загруженные пользователями, — политика со списком доменов его не остановит. Надёжнее разрешать скрипты по nonce или хешу, а не по списку доменов.
Но CSP — это не просто заголовок, который можно включить и забыть. Если включить его сразу в режиме запрета, сайт сломается. Поэтому CSP внедряют в три этапа:
- Режим отчёта (
Content-Security-Policy-Report-Only). Браузер не блокирует ресурсы, но отправляет отчёты о нарушениях на указанный URL. Это позволяет понять, какие ресурсы нарушают политику, и исправить их до включения запрета. - Разбор нарушений. На этом этапе нужно проанализировать отчёты и исправить проблемы: заменить встроенные скрипты на внешние, добавить разрешения для сторонних доменов, обновить библиотеки.
- Режим запрета (
Content-Security-Policy). Только после того, как все нарушения устранены, можно включить CSP в режиме запрета.
Пример CSP в режиме отчёта:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' https://cdn.example.com; report-uri /csp-report-endpoint
Что ломается при включении CSP:
- Встроенные скрипты. Их нужно вынести в отдельные файлы.
- Сторонние виджеты. Для них нужно добавить разрешение на загрузку с их домена.
- Аналитика. Для неё тоже нужно добавить разрешение на загрузку с домена аналитического сервиса.
- Стили и шрифты, загружаемые с CDN. Их нужно либо разместить на своём домене, либо добавить разрешение на загрузку с CDN.
Strict-Transport-Security (HSTS)
HSTS — это заголовок, который принудительно переводит все соединения на HTTPS. Он нужен, чтобы предотвратить атаки через понижение протокола (downgrade attacks): если злоумышленник сможет перехватить соединение и заставить браузер использовать HTTP, он сможет украсть куки или внедрить вредоносный код.
HSTS работает так: когда браузер получает этот заголовок, он запоминает, что сайт должен открываться только по HTTPS, и автоматически переводит все HTTP-запросы на HTTPS. Заголовок выглядит так:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Что означают параметры:
max-age— время, в течение которого браузер будет помнить о необходимости использовать HTTPS. Срок отсчитывается заново при каждом ответе с заголовком по HTTPS и со сроком сертификата не связан. Со слишком коротким сроком после его истечения первый запрос снова уходит по HTTP и может быть перехвачен. Со слишком длинным браузеры посетителей будут требовать HTTPS до конца срока, а снять правило можно только ответомmax-age=0по работающему HTTPS.includeSubDomains— распространяет правило на все поддомены.preload— сам по себе сайт никуда не вносит: это согласие владельца на включение домена в предзагруженный список HSTS. В список сайт попадает только после отдельной заявки и выхода версий браузеров, в которые список вшит. После этого браузер знает о необходимости HTTPS ещё до первого посещения. Чтобы заявку приняли, домен должен соответствовать требованиям: достаточно длинныйmax-age, наличиеincludeSubDomains, сама директиваpreload, работающий HTTPS на всех поддоменах и перенаправление с HTTP на HTTPS.
Что ломается при включении HSTS:
- Если на сайте есть страницы, которые должны открываться по HTTP, они перестанут работать.
- Если сертификат SSL недействителен, браузер не сможет открыть сайт вообще.
- Если сайт добавлен в предзагруженный список HSTS, его нельзя будет удалить оттуда без обновления всех браузеров.
X-Frame-Options
Этот заголовок запрещает встраивать страницу в рамку (<iframe>). Он нужен, чтобы предотвратить кликджекинг: атаку, при которой злоумышленник встраивает вашу страницу в свою и обманом заставляет пользователя нажать на кнопку.
Заголовок выглядит так:
X-Frame-Options: DENY
Или так:
X-Frame-Options: SAMEORIGIN
Что означают параметры:
DENY— запрещает встраивание страницы в рамку вообще.SAMEORIGIN— разрешает встраивание только на страницах того же источника.
Что ломается при включении X-Frame-Options:
- Если на сайте есть страницы, которые должны встраиваться в рамки, они перестанут работать.
- Если сайт использует сторонние сервисы, которые встраивают его в рамки, они тоже перестанут работать.
X-Content-Type-Options
Этот заголовок с единственным значением nosniff запрещает браузеру угадывать тип содержимого вопреки объявленному сервером. Браузеры его соблюдают: скрипт или стиль, отданный с неподходящим Content-Type, при nosniff не выполняется и не применяется. Без заголовка браузер может выполнить как скрипт файл, который сервер отдал как текст, а файл без указанного типа — показать как страницу. Так и срабатывает атака через загруженный пользователем файл. Заголовок не устарел, и ломает он только ресурсы, отданные с неверным типом: их нужно отдавать с правильным Content-Type.
Referrer-Policy
Этот заголовок контролирует, какую информацию о предыдущей странице браузер отправляет в заголовке Referer. Он нужен, чтобы предотвратить утечку конфиденциальной информации: например, если пользователь переходит с страницы с параметрами в URL, эти параметры могут попасть в логи сторонних сервисов.
Заголовок выглядит так:
Referrer-Policy: strict-origin-when-cross-origin
Что означают параметры:
no-referrer— не отправлять заголовокRefererвообще.no-referrer-when-downgrade— не отправлятьRefererпри переходе с HTTPS на HTTP.same-origin— отправлятьRefererтолько при переходах внутри одного источника.strict-origin— отправлять только источник (схему, хост и порт) при любых переходах, в том числе к тому же источнику, и ничего не отправлять при переходе с HTTPS на HTTP.strict-origin-when-cross-origin— отправлять полный URL при переходах внутри одного источника, только источник при переходах на другие источники и ничего не отправлять при переходе с HTTPS на HTTP.
Что ломается при включении Referrer-Policy:
- Если на сайте есть аналитика, которая использует заголовок
Refererдля отслеживания переходов, она может перестать работать. - Если сайт использует сторонние сервисы, которые полагаются на
Referer, они тоже могут перестать работать.
Какие заголовки ничего не делают
Некоторые заголовки безопасности остались от прошлых версий спецификаций и не влияют на поведение браузера. Вот основные из них:
X-XSS-Protection
Этот заголовок должен был включать встроенную защиту от XSS в браузерах, но современные браузеры игнорируют его. Вместо него используют CSP.
Expect-CT
Этот заголовок должен был проверять сертификаты на соответствие Certificate Transparency, но современные браузеры делают это автоматически. Заголовок устарел и не влияет на поведение браузера.
Порядок внедрения заголовков безопасности
Внедрять заголовки безопасности нужно постепенно, чтобы не сломать сайт. Вот порядок действий:
- Включить режим отчёта для CSP. Это позволит понять, какие ресурсы нарушают политику, и исправить их до включения запрета.
- Проанализировать отчёты CSP. На этом этапе нужно исправить все нарушения: заменить встроенные скрипты на внешние, добавить разрешения для сторонних доменов, обновить библиотеки.
- Включить CSP в режиме запрета.
- Включить HSTS. Начать с небольшого
max-age, чтобы проверить, что правило работает корректно, и постепенно увеличивать его. - Включить
X-Frame-Options. Начать сSAMEORIGIN, чтобы не сломать виджеты на своём домене. - Включить
X-Content-Type-Options. Установить значениеnosniff, чтобы браузер не угадывал тип содержимого. - Включить
Referrer-Policy. Начать сstrict-origin-when-cross-origin, чтобы не сломать аналитику.
Как проверить, что заголовки действительно отдаются
Заголовки безопасности должны отдаваться на всех страницах сайта, а не только на главной. Чтобы проверить это, можно использовать инструменты разработчика в браузере:
- Открыть инструменты разработчика.
- Перейти на вкладку «Network» (Сеть).
- Обновить страницу.
- Найти запрос к странице и проверить заголовки ответа.
Или использовать командную строку:
curl -I https://example.com/path/to/page
Что проверять:
- Заголовки должны отдаваться на всех страницах, а не только на главной. Для этого нужно проверить несколько страниц с разными путями.
- Заголовки должны быть корректно настроены. Например, CSP не должен блокировать нужные ресурсы, а HSTS должен иметь адекватное значение
max-age. - Заголовки не должны конфликтовать друг с другом.
Скопировать чужой набор заголовков целиком — плохая идея
Часто встречается совет: «Возьмите набор заголовков безопасности с чужого сайта и поставьте у себя». Это плохая идея, потому что:
- Чужой набор может не подходить вашему сайту. Например, если на чужом сайте нет встроенных скриптов, а на вашем есть, CSP сломает их.
- Чужой набор может быть устаревшим. Например, если на чужом сайте стоит
X-XSS-Protection, а современные браузеры его игнорируют. - Чужой набор может конфликтовать с вашими сервисами. Например, если на чужом сайте стоит
Referrer-Policy: no-referrer, а ваша аналитика используетReferer, она перестанет работать. - Чужой набор может быть избыточным.
Вместо этого нужно настраивать заголовки безопасности под свой сайт, постепенно и с проверкой.
Позиция редакции
Заголовки безопасности — это важный инструмент, но они не заменяют другие меры защиты. Их нужно настраивать постепенно, с проверкой и исправлением нарушений. Не стоит копировать чужой набор заголовков целиком: лучше разобраться, что каждый из них делает, и настроить их под свой сайт.
Заголовки безопасности эффективны только при соблюдении следующих условий:
- Уязвимости в коде исправляются, а не прикрываются заголовками: заголовки сужают то, что можно сделать через ошибку, но не убирают её.
- Сертификат SSL действителен и настроен корректно.
- На сайте используются актуальные библиотеки и фреймворки.
- Настройки сервера соответствуют требованиям безопасности.
Позиция редакции неверна, если:
- На сайте нет реальных угроз, которые можно предотвратить с помощью заголовков безопасности. Например, если сайт статический и не содержит пользовательского ввода, XSS и кликджекинг ему не страшны.
- Заголовки безопасности мешают работе сайта больше, чем защищают его. Например, если CSP блокирует критически важные ресурсы, а исправить это невозможно, лучше отключить CSP, чем оставить сайт неработающим.
- На сайте уже реализованы более эффективные меры защиты. Например, если все страницы собираются шаблонизатором, который экранирует любой вывод, а сторонних скриптов на сайте нет, строгая CSP остаётся страховкой на случай ошибки, а не основной защитой, и её внедрение можно отложить.