HTTP-заголовки безопасности: CSP, HSTS, X-Frame-Options
Разбираем, какие HTTP-заголовки реально защищают сайт от XSS, кликджекинга и подмены протокола, как добавить их в конфигурацию Nginx и как проверить, что они действительно применяются.
Зачем нужны заголовки безопасности, если сайт уже работает по HTTPS
HTTPS решает одну конкретную задачу — шифрует канал передачи данных между браузером и сервером, не давая перехватить или подменить трафик по пути. Это важная, но далеко не единственная категория угроз для сайта: злоумышленник может внедрить вредоносный скрипт прямо в код страницы через уязвимость в форме комментариев, встроить чужой сайт в невидимый фрейм поверх вашего интерфейса, или заставить браузер интерпретировать загруженный файл не так, как задумывалось.
HTTP-заголовки безопасности — это набор инструкций, которые сервер передаёт браузеру вместе с обычным ответом, и которые говорят браузеру, как именно вести себя с полученной страницей: каким источникам скриптов и стилей доверять, разрешено ли встраивать страницу в чужой фрейм, стоит ли переходить на HTTPS принудительно даже если пользователь ввёл адрес без протокола. Браузер сам обеспечивает эту защиту на основе полученных инструкций — сервер не делает ничего дополнительного вычислительно, кроме отправки нескольких дополнительных строк в ответе.
Ценность этих заголовков в том, что они не требуют переписывания кода сайта или CMS — это конфигурация на уровне веб-сервера, применимая централизованно ко всем страницам сразу. При этом эффект от них ощутим именно в сценарии реальной атаки — типичный посетитель никогда не заметит их присутствия, но при попытке эксплуатации XSS-уязвимости или кликджекинга разница между их наличием и отсутствием может определить, состоится атака или нет.
Content-Security-Policy: главный барьер против XSS
Content-Security-Policy (CSP) — самый мощный и одновременно самый сложный в настройке из всех заголовков безопасности. Он явно перечисляет, из каких источников браузеру разрешено загружать скрипты, стили, изображения, шрифты и другие ресурсы, и по умолчанию блокирует инлайновые скрипты, встроенные прямо в HTML, — именно инлайновые скрипты чаще всего используются при эксплуатации межсайтового скриптинга (XSS), когда злоумышленник добавляет вредоносный код через уязвимую форму или комментарий.
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;" always;
Директива default-src задаёт правило по умолчанию для всех типов ресурсов, а более специфичные директивы вроде script-src или img-src переопределяют его для конкретного типа. Значение 'self' разрешает загрузку только с того же домена, что и сама страница; конкретные внешние домены (CDN, платёжные виджеты, аналитика) нужно перечислить явно — CSP работает по принципу белого списка, и всё, что не разрешено явно, браузер заблокирует.
Честная сложность CSP в том, что для реального сайта с подключёнными сторонними сервисами — картами, чатами поддержки, платёжными виджетами, аналитикой — правильный список разрешённых источников редко получается настроить с первого раза. Практичный подход — сначала включить CSP в режиме Content-Security-Policy-Report-Only, который не блокирует ничего, а только логирует нарушения, посмотреть реальные отчёты о том, что было бы заблокировано, донастроить список источников по этим данным, и только затем переключить заголовок в полноценный блокирующий режим.
Частая ошибка — включить CSP один раз, отладить на главной странице и забыть, что другие разделы сайта могут подключать собственные скрипты и стили, не учтённые в исходной политике. При любом добавлении нового стороннего виджета или скрипта на сайт CSP стоит пересматривать — иначе он неожиданно начнёт ломать функциональность, которая до этого работала.
Strict-Transport-Security: запрет на откат к незашифрованному HTTP
Даже если сайт настроен на редирект с HTTP на HTTPS, первый запрос браузера при вводе адреса без протокола всё равно на короткое мгновение уходит по незашифрованному HTTP, прежде чем сервер успевает ответить редиректом, — и именно на этом окне строится атака вида SSL stripping, когда злоумышленник, находясь между пользователем и сервером (например, в открытой Wi-Fi сети), перехватывает этот первый запрос и не даёт браузеру перейти на HTTPS вообще.
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
Заголовок Strict-Transport-Security (HSTS) решает эту проблему на уровне самого браузера: получив этот заголовок один раз, браузер запоминает, что для этого домена на протяжении срока max-age (в примере — один год в секундах) нужно всегда обращаться напрямую по HTTPS, даже если пользователь явно ввёл адрес с http:// или перешёл по старой ссылке без протокола, — без единого промежуточного незащищённого запроса.
Параметр preload — заявка на включение домена в специальный список, зашитый прямо в код браузеров Chrome, Firefox и других, который проверяется ещё до первого обращения к сайту вообще, устраняя даже то самое первое окно уязвимости. Важная и необратимая на практике оговорка: включение домена в этот список — решение на годы вперёд, поскольку список распространяется с обновлениями браузеров по всему миру, и обратное исключение домена из preload-списка занимает месяцы и не гарантирует мгновенного эффекта на уже установленных у пользователей браузерах.
Именно поэтому включать preload стоит только тогда, когда сайт точно и на длительный срок остаётся исключительно на HTTPS без исключений — если существует хоть какой-то поддомен или сценарий, где HTTPS пока не настроен, preload может неожиданно сделать его полностью недоступным для части посетителей.
X-Frame-Options и frame-ancestors: защита от кликджекинга
Кликджекинг (clickjacking) — атака, при которой злоумышленник встраивает ваш сайт в невидимый iframe поверх собственной страницы с приманкой, так что пользователь, кликая по видимой кнопке чужого сайта, на самом деле кликает по невидимому элементу вашего сайта под ней — например, по кнопке подтверждения перевода средств или изменения настроек аккаунта, о существовании которой пользователь даже не подозревает.
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "frame-ancestors 'self';" always;
X-Frame-Options со значением SAMEORIGIN разрешает встраивание сайта во фрейм только страницами того же домена, полностью запрещая это для любых внешних сайтов. Более современная директива frame-ancestors внутри CSP делает то же самое, но гибче — можно явно перечислить конкретные доверенные домены, которым разрешено встраивать сайт в свой фрейм, что полезно, например, для встраивания собственного виджета на партнёрских сайтах.
На практике стоит указывать оба заголовка одновременно: frame-ancestors — более новый и гибкий стандарт, но X-Frame-Options по-прежнему поддерживается более широким кругом старых браузеров и клиентов, где поддержка соответствующей директивы CSP может отсутствовать или работать иначе. Указание обоих заголовков не создаёт конфликта — современный браузер просто применит более специфичную директиву CSP, а старый воспользуется классическим заголовком.
X-Content-Type-Options: запрет угадывать тип содержимого
Некоторые старые браузеры при получении файла пытаются самостоятельно угадать его реальный тип по содержимому, даже если сервер явно указал другой Content-Type в заголовке ответа — эта функция называется MIME sniffing и изначально задумывалась как удобство для случаев, когда сервер настроен неверно. Проблема в том, что это открывает возможность для атаки: если злоумышленнику удаётся загрузить файл, замаскированный под безобидное изображение, но содержащий исполняемый JavaScript-код, браузер при угадывании типа может решить исполнить его как скрипт.
add_header X-Content-Type-Options "nosniff" always;
Заголовок X-Content-Type-Options со значением nosniff прямо запрещает браузеру это поведение — файл интерпретируется строго в соответствии с тем Content-Type, который явно указал сервер, и ни в каком другом качестве. Это один из самых простых заголовков для внедрения: он практически никогда не ломает легитимную функциональность сайта, поскольку требует лишь того, чтобы сервер сам верно и последовательно указывал типы отдаваемых файлов, что для правильно настроенного Nginx выполняется автоматически.
Referrer-Policy: что сайт сообщает о том, откуда пришёл посетитель
При переходе по ссылке с одной страницы на другую браузер по умолчанию передаёт целевому сайту заголовок Referer с полным адресом страницы, с которой был совершён переход — включая иногда чувствительные данные в самом URL, например параметры поиска, токены сессии в устаревших схемах авторизации или внутренние пути административных разделов.
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
Значение strict-origin-when-cross-origin — практичный компромисс для большинства сайтов: при переходе внутри одного и того же домена передаётся полный адрес страницы (что полезно для внутренней аналитики), при переходе на другой домен передаётся только сам домен без пути и параметров запроса, а при переходе с HTTPS на менее защищённый HTTP заголовок Referer не передаётся вовсе, чтобы не раскрывать данные протоколу без шифрования.
Более строгие значения вроде no-referrer полностью убирают этот заголовок при любом переходе, что максимально защищает приватность посетителей, но одновременно лишает сайты-получатели трафика информации об источнике перехода — это стоит учитывать, если для бизнеса важна атрибуция источников трафика во внешней аналитике партнёрских программ или рекламных кабинетов.
Как добавить все заголовки разом в конфигурации Nginx
На практике удобнее не разбрасывать директивы add_header по разным блокам location, а собрать их в одном месте — на уровне server-блока сайта, откуда они автоматически наследуются во всех вложенных location, если явно не переопределены. Ниже — сводный пример типичного набора заголовков для сайта на PHP-CMS с HTTPS.
server {
listen 443 ssl http2;
server_name example.ru;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" always;
}
Обратите внимание на параметр always в конце каждой директивы add_header — без него Nginx по умолчанию добавляет заголовок только к ответам с кодом 200, 201, 204, 206, 301, 302, 303, 304 или 307, и заголовок молча пропадает из ответов с другими кодами, например 404 или 500, что может остаться незамеченным при поверхностной проверке главной страницы, но проявится при аудите безопасности всего сайта.
Ещё одна практическая деталь: если один и тот же заголовок добавлен и в основном server-блоке, и в конкретном location, Nginx использует только более специфичное значение из location, полностью игнорируя внешнее — директивы add_header не суммируются и не наследуются частично, поэтому при переопределении одного заголовка в конкретном блоке нужно продублировать туда и остальные, если они там тоже нужны.
Проверка: не верьте конфигурации на слово
Сохранённый и даже перезагруженный конфигурационный файл Nginx не гарантирует, что заголовки реально доходят до браузера — опечатка в синтаксисе, конфликт с другим блоком конфигурации или кэширующий прокси между сервером и пользователем способны молча исказить или вовсе убрать ожидаемый заголовок из финального ответа.
curl -I https://example.ru
Команда curl -I отправляет HEAD-запрос и показывает полный набор заголовков ответа сервера — этого достаточно, чтобы за секунды убедиться, что каждый из настроенных заголовков безопасности действительно присутствует в ответе именно с тем значением, которое ожидалось, а не осталось только в конфигурационном файле. Проверять стоит не только главную страницу, но и хотя бы одну внутреннюю страницу и страницу с ошибкой 404 — заголовки, забытые в отдельном location или зависящие от кода ответа, на главной странице могут выглядеть настроенными правильно, а на остальных отсутствовать.
Дополнительно полезно открыть вкладку «Сеть» (Network) в инструментах разработчика браузера и посмотреть заголовки реального запроса страницы — это также покажет, не блокирует ли CSP какие-либо ресурсы страницы прямо сейчас: браузер выводит подробное сообщение в консоли о каждом заблокированном источнике с указанием, какая именно директива CSP сработала, что заметно ускоряет отладку по сравнению с гаданием, какое именно правило политики оказалось слишком строгим.
Типичные ошибки при внедрении заголовков безопасности
Включение строгого CSP сразу в боевом режиме без предварительного тестирования в режиме Content-Security-Policy-Report-Only — типичная причина того, что часть сайта внезапно перестаёт работать: платёжный виджет, встроенная карта или сторонний чат поддержки просто перестают загружаться, поскольку их источники не были заранее внесены в список разрешённых.
Включение HSTS preload для домена, который используется не только для готового production-сайта, но и, например, для тестового окружения на поддоменах без HTTPS — includeSubDomains распространяет требование на все поддомены без исключения, и любой из них, работающий пока без сертификата, станет полностью недоступен.
Забытый параметр always в директиве add_header — заголовок применяется только к части ответов сервера, что создаёт ложное ощущение полной защиты при поверхностной проверке главной страницы сайта.
Полное игнорирование заголовков безопасности из соображений «сайт маленький, кому он нужен» — на практике автоматизированные сканеры атакуют не конкретные сайты выборочно, а весь диапазон адресов подряд в поисках известных уязвимостей, и размер или популярность сайта здесь ни на что не влияют.
Итог: с чего начать, если заголовков ещё нет вообще
Если ни один из этих заголовков ещё не настроен, разумная последовательность внедрения такая: сначала X-Content-Type-Options и X-Frame-Options — они почти никогда ничего не ломают и внедряются буквально за одну строку конфигурации каждый. Следом Strict-Transport-Security с умеренным max-age без preload, пока не появится полная уверенность, что весь сайт и все его поддомены постоянно работают по HTTPS.
Content-Security-Policy стоит внедрять последним и обязательно через режим Report-Only на старте — это единственный из перечисленных заголовков, который требует реального тестирования под конкретный сайт с его сторонними скриптами и виджетами, прежде чем включать в блокирующем режиме. После добавления каждого заголовка проверяйте результат командой curl -I, а не полагайтесь на то, что конфигурация была применена правильно только потому, что Nginx перезагрузился без ошибок.
Если разбираться со всем этим самостоятельно нет времени — опишите задачу в заявке внизу страницы, настроим полный набор заголовков безопасности под конкретный стек сайта, включая корректную политику CSP для уже используемых сторонних сервисов.
Почему стоит выбрать нас
Быстрый запуск
Подберём конфигурацию и развернём проект в день обращения.
Надёжность
NVMe-диски, защита от DDoS и резервные копии по расписанию.
Поддержка 24/7
Дежурные инженеры на связи в любое время суток.
Настройка под ключ
Установим CMS, настроим веб-сервер, SSL и почту за вас.
Перенос без простоя
Аккуратно перенесём сайт и базу данных на новый сервер.
Прозрачные цены
Актуальные тарифы и честный подбор без навязывания.
Вопросы и ответы
Достаточно ли одних заголовков безопасности, чтобы считать сайт защищённым?
Нет, это один из уровней защиты, а не единственный — заголовки снижают ущерб от определённых классов атак (XSS, кликджекинг, подмена протокола), но не заменяют своевременные обновления CMS и плагинов, безопасный код и защиту сервера в целом.
Может ли неправильно настроенный CSP сломать сайт?
Да, и это самая частая проблема на практике — слишком строгая политика блокирует легитимные скрипты, стили или шрифты, включая сторонние виджеты аналитики и оплаты, если их источники не были явно перечислены в директивах CSP.
Что произойдёт, если добавить домен в HSTS preload список, а потом решить вернуться на HTTP?
Домен останется недоступен по HTTP во всех браузерах, которые используют предзагруженный список Chromium, Firefox и других — исключение из списка возможно, но занимает недели или месяцы и требует времени на распространение обновлённого списка по браузерам.
Нужны ли эти заголовки, если у сайта уже настроен SSL-сертификат?
Да, SSL-сертификат шифрует канал передачи данных, но не защищает от XSS, кликджекинга или подмены типа содержимого — это разные классы угроз, требующие разных мер защиты, и заголовки безопасности дополняют, а не дублируют HTTPS.
Какой заголовок стоит настроить в первую очередь, если нет времени на все?
Strict-Transport-Security и X-Content-Type-Options дают заметный эффект при минимальном риске что-либо сломать; Content-Security-Policy даёт наибольшую защиту от XSS, но требует более внимательной настройки и тестирования перед включением в боевом режиме.
Как проверить заголовки безопасности без специальных инструментов?
Командой curl -I с адресом сайта — она покажет все заголовки ответа сервера, включая заголовки безопасности, без необходимости открывать браузер или устанавливать дополнительные утилиты.
Работает ли frame-ancestors в старых браузерах, где нет поддержки CSP?
Нет, в этом случае стоит дополнительно указать классический заголовок X-Frame-Options — он поддерживается заметно более широким кругом браузеров и служит запасным вариантом там, где CSP frame-ancestors ещё не поддерживается.
Может ли Referrer-Policy повлиять на аналитику сайта?
Да, слишком строгая политика (например, no-referrer) скрывает от аналитики сайта-получателя, откуда пришёл посетитель, что усложняет анализ источников трафика — для большинства сайтов разумным компромиссом считается strict-origin-when-cross-origin.
Как оплачивать услуги?
Оплата помесячная, для облачных серверов доступна почасовая тарификация. Принимаются карты, банковские переводы и электронные кошельки.
Есть ли круглосуточная поддержка?
Да, дежурные инженеры на связи 24/7.
Выдаёте ли закрывающие документы?
Да, предоставим необходимые документы для оплаты.
Как оставить заявку?
Заполните форму на странице или нажмите «Заказать» — мы свяжемся и поможем с подбором.