17 янв. 2025 г.·6 мин чтения

Заголовки безопасности для SaaS: разумные дефолты, которые можно внедрять

Узнайте про заголовки безопасности для SaaS: безопасные дефолты для cookie и заголовков, которые блокируют распространённые атаки, упрощают развёртывание и делают поведение браузера предсказуемым.

Заголовки безопасности для SaaS: разумные дефолты, которые можно внедрять

Почему эти настройки важны

Небольшие браузерные настройки блокируют удивительно много типичных атак ещё до того, как ваша команда дотронется до кода приложения. Одна забытая флаг‑опция cookie, слишком свободная политика скриптов или единственная страница, позволяющая небезопасный контент, могут превратить мелкую ошибку в украденную сессию или фальшивую форму входа. Закрытие таких дыр на этапе разработки часто защищает лучше, чем поспешный патч после запуска.

Эти слабые места повторяются снова и снова. Страницы входа привлекают злоумышленников, потому что одновременно обрабатывают пароли, токены, редиректы и сторонние скрипты. Сессионные cookie создают проблемы, когда их отправляют без нужды, когда они доступны клиентскому коду или пересекают границы сайтов без причины. Встроенные теги скриптов несут риск: один скомпрометированный тег может выполнить код в вашем приложении. Смешанный контент кажется устаревшей темой, но одна небезопасная картинка, скрипт или iframe всё ещё может ослабить страницу, которую пользователи считали защищённой.

Даже простое SaaS‑приложение имеет достаточно поверхности атаки, чтобы причинить реальный вред. Достаточно дашборда, страницы биллинга и потока входа. Если браузер по умолчанию отказывает в небезопасном поведении, вы получаете тихий слой защиты каждый раз, когда кто‑то входит в систему.

Команды обычно попадают в беду, когда меняют слишком много браузерных настроек за один релиз. Жёсткая Content Security Policy может заблокировать скрипты, отвечающие за форму входа. Изменение cookie может сломать single sign‑on, запоминание сессии или встроенные виджеты поддержки. HSTS при неосторожном разворачивании может «закрепить» плохой редирект в браузере. На бумаге приложение выглядит безопаснее, но реальные пользователи не могут загрузить страницы или войти в систему.

Лучшая цель проста: блокировать лёгкие атаки, сохранять работу приложения и позволять пользователям входить без странных ошибок браузера. Начните с защит, которые редко ломают поведение продукта, следите за тем, что падает, и ужесточайте правила по шагам. Это обычно лучше, чем масштабный пуш безопасности в пятницу, который съест выходные.

С чего начать

Начните с HTTPS везде. Если какая‑то страница входа, ресурс, callback или админская страница всё ещё загружается по HTTP, исправьте это прежде всего. Заголовки и флаги cookie помогают, но они мало что сделают, если само соединение уязвимо к подмене.

Принудительно редиректьте HTTP на HTTPS и отправляйте аутентификационные cookie только по зашифрованным запросам. Затем тестируйте приложение из чистой сессии браузера, а не с машины разработчика, заваленной старыми исключениями и кешем.

Далее — cookie, и разные cookie требуют разных правил. Сессионная cookie, которая держит пользователя в системе, не должна иметь те же права, что cookie с настройками темы или аналитикой.

Для большинства SaaS‑продуктов такая база работает хорошо:

  • Сессионная cookie: Secure, HttpOnly, SameSite=Lax
  • Cookie предпочтений: Secure, SameSite=Lax
  • Кросс‑сайтовая cookie только при реальной необходимости: SameSite=None; Secure
  • Разделяйте сессионные cookie и UI/трековые cookie
  • Используйте минимально разумное время жизни для чувствительных cookie

Это разделение убирает много путаницы. Приложение может надёжно защищать состояние входа, при этом фронтенд всё ещё читать безопасную cookie с настройками при необходимости.

Обычный пример — дашборд с входом по email и обычным веб‑интерфейсом. В таком случае SameSite=Lax обычно самый безопасный дефолт для сессионной cookie. Он блокирует много неосмотрительного кросс‑сайтового поведения, не ломая обычную навигацию. SameSite=Strict часто слишком жёсткий, а None — слишком свободный без явной нужды.

После cookie добавьте небольшой набор ответных заголовков и остановитесь, пока они не заработают чисто. Начните с Content-Security-Policy, HSTS и правил фреймов, таких как frame-ancestors или X-Frame-Options. CSP снижает риск внедрения скриптов. HSTS говорит браузеру держаться HTTPS. Правила фреймов помогают блокировать clickjacking.

Не собирайте редкие заголовки только потому, что сканер на них жалуется. Редкие или устаревшие настройки быстро превращают конфигурацию в шум. Закрепите базу, а затем ужесточайте то, что реально нужно вашему приложению.

Cookie заслуживают того же внимания, что и заголовки. Если cookie может держать пользователя в системе, относитесь к ней как к секрету, а не как к удобству.

Устанавливайте каждая сессионная cookie с Secure. Это говорит браузеру отправлять её только по HTTPS, так что простой HTTP‑запрос не протечёт сессию случайно. Добавьте HttpOnly. Даже если страница подхватит баг в скрипте, этот скрипт не сможет прочитать cookie и переслать её куда‑то ещё.

SameSite требует немного размышлений. Для большинства SaaS‑флоу входа SameSite=Lax — правильный дефолт. Он препятствует многим кросс‑сайтовым запросам с передачей cookie, но при этом работает при обычной навигации. Проблемы обычно возникают в сценариях с переходами между доменами, например в некоторых SSO‑настройках, входе по «магическим» ссылкам или возвратах с платежных сервисов. Тестируйте эти пути в реальных браузерах, прежде чем ослаблять настройку.

Держите область действия cookie узкой. Если ваш продукт работает на app.example.com, не делитесь auth‑cookie со всеми поддоменами, если в этом нет реальной необходимости. То же и с Path. Узкая область снижает случайное раскрытие. Если маркетинговые страницы находятся на www, а приложение — на app, то cookie входа обычно должна оставаться на app.

Правила истечения срока важны не меньше. Сессионная cookie должна завершаться вместе с сессией, и сервер должен отклонять её после ясного таймаута. Cookie «запомнить меня» должна быть отдельной, жить дольше по замыслу и легко отзывать. Не держите аутентификационные cookie включёнными месяцами из удобства. Короткая жизнь может раздражать нескольких пользователей, но украденная долго‑живая cookie приведёт к куда более серьёзным последствиям.

Заголовки, дающие наибольшую ценность

Небольшой набор заголовков блокирует многие низкоэффортные веб‑атаки. Начните с тех, что защищают входы, уменьшают утечки данных и не требуют недель споров.

HSTS должно быть вверху списка. Оно говорит браузеру использовать только HTTPS, что мешает трюкам по понижению уровня безопасности и случайным небезопасным запросам. Включайте HSTS только после проверки HTTPS на всех поддоменах, которые вы обслуживаете, включая старые админ‑хосты, инструменты поддержки и staging‑домены, до которых пользователи ещё могут добраться. Если один поддомен не готов, HSTS может превратить небольшую ошибку конфигурации в простой.

Content Security Policy умеет многое, но она также может быстро сломать работающее приложение. Начните с Content-Security-Policy-Report-Only и смотрите, что реально загружается на странице. Inline‑скрипты, виджеты чата третьих сторон, старые теги аналитики и инструменты предпросмотра файлов часто всплывают здесь. После их очистки переходите к принудительному режиму.

Ограничения фреймов особенно важны на страницах входа, биллинга и в админке. Если другой сайт может встраивать эти страницы в невидимый фрейм, clickjacking становится проще. frame-ancestors 'none' — самый чистый выбор для страниц, которые никогда не должны появляться внутри другого сайта. Если нужно встраивание в тех же пределах сайта, используйте frame-ancestors 'self' для таких участков.

Хорошая база обычно выглядит так:

  • HSTS после полной проверки HTTPS на всех поддоменах
  • CSP в режиме report‑only перед принудительным применением
  • frame-ancestors, установленный для блокировки ненужного встраивания
  • X-Content-Type-Options: nosniff
  • Referrer-Policy: strict-origin-when-cross-origin

X-Content-Type-Options: nosniff — лёгкая победа. Он мешает браузерам угадывать типы файлов, что закрывает ряд проблем с обработкой скриптов и ресурсов. Большинство команд должны выпустить его рано.

Для рефереров держите баланс практичным. strict-origin-when-cross-origin обычно даёт достаточно аналитического контекста, не отправляя полные пути и query‑строки на внешние сайты. Это значит, что вы всё ещё можете измерять источники трафика, не раскрывая reset‑токены, внутренние идентификаторы или поисковые запросы, спрятанные в URL.

Как развернуть это без поломок

See Risk Across Your Stack
Check how your app, subdomains, and third-party tools affect browser security rules.

Большинство провалов при внедрении заголовков происходят по одной причине: команды меняют слишком многое одновременно. Если вы включаете строгие cookie, новую CSP и HSTS в одном релизе, вы потратите часы, угадывая, какое изменение сломало вход или сторонний виджет.

Начните с картирования того, что приложение реально загружает в браузере. Запишите каждый скрипт, шрифт, картинку, фрейм и API‑эндпоинт, включая страницы оплаты, аналитику, чат‑виджеты и всё, что на кастомных поддоменах. Сюрпризы прячутся в местах, о которых забывают: страница сброса пароля, портал биллинга или встроенная система поддержки.

Медленное развертывание работает лучше:

  • Делайте настройки cookie одинаковыми в dev, staging и prod
  • Включите CSP в режиме report‑only сначала
  • Просмотрите отчёты и уберите ненужные источники
  • Закрепляйте по одному заголовку, затем снова тестируйте

Шаг с cookie важнее, чем многие думают. Если в staging используются свободные по умолчанию настройки, а в production — Secure, HttpOnly и правила SameSite, вы не тестируете реальное приложение. Держите правила согласованными, чтобы поток входа, который работает в staging, не упал после релиза.

CSP требует терпения. Report‑only позволяет браузеру сказать, что бы сломалось, не блокируя это пока ещё. Через несколько дней вы обычно находите старые источники скриптов, забытые хосты изображений или виджет поддержки, который тянет ресурсы с двух дополнительных доменов. Обрежьте этот список, прежде чем переходить в режим принудительного блокирования.

Не переводите все заголовки из мягкого режима в строгий в один день. Выберите один, примените строгую проверку и протестируйте сценарии, которые больше всего страдают при ошибке:

  • Вход и выход из системы
  • Сброс пароля
  • Биллинг и возврат с оплаты
  • Встроенные виджеты чата или поддержки
  • Админские страницы, использующие старые скрипты

Небольшая команда SaaS может завершить это за несколько коротких релизов, а не за один рискованный запуск. Обычно это лучший компромисс: меньше сюрпризов, чище политики и гораздо меньше общения со службой поддержки после деплоя.

Простой пример для SaaS

Представьте небольшое B2B SaaS‑приложение. Пользователи входят по email, платят через Stripe и открывают виджет помощи из дашборда, когда застревают. Команда хочет базовую защиту от распространённых веб‑атак, но не хочет, чтобы каждый деплой превращался в охоту за багами браузера.

Начните с сессионной cookie. Установите её как Secure, HttpOnly и SameSite=Lax. Это значит, что браузер отправляет её только по HTTPS, скрипты страниц не могут её прочитать, а обычная навигация всё ещё работает для большинства флоу входа. Затем протестируйте весь путь входа, включая редирект обратно из email‑ссылки и страницы биллинга.

Большинству команд стоит начать с небольшой белого списка источников, а не с огромного. Страницы приложения должны загружать скрипты и фреймы только с вашего домена, поставщика платежей и утверждённого виджета поддержки. Если после этого перестанет работать какой‑то старый кусок аналитики или чат, это полезный сигнал: страница доверяла третьим сторонам сильнее, чем команда думала.

Разделяйте публичные и приватные страницы. Маркетинговая страница может позволять встроенное демо, видео или планировщик. Админская зона не должна позволять фрейм‑встраивание вообще. Биллинг, управление пользователями и страницы аудита — последнее место, где вы хотите риск clickjacking.

Простая настройка часто выглядит так:

  • Сессионная cookie: Secure, HttpOnly, SameSite=Lax
  • CSP приложения: разрешать скрипты и фреймы с self, Stripe и утверждённого виджета поддержки
  • Админские страницы: блокировать любое встраивание в iframe
  • Публичные маркетинговые страницы: разрешать только нужные им встраивания

Затем тестируйте потоки, которые люди действительно используют, на реальных телефонах. Откройте письмо для сброса пароля в мобильном почтовом приложении, нажмите ссылку, войдите по магической ссылке, посетите биллинг и выйдите. Safari и Chrome на мобильных часто выдают мелкие ошибки, которые десктоп‑тестирование пропускает, особенно вокруг редиректов и обработки cookie.

Если после выхода остаётся старая вкладка активной или магическая ссылка работает только в одном браузере — исправьте это до релиза. Пользователи заметят такие баги намного раньше, чем аккуратную политику заголовков.

Ошибки, которые делают команды

Roll Out Changes Safely
Map a safer header and cookie rollout without breaking sign-in, billing, or support tools.

Команды часто выбирают самый строгий вариант и думают, что так безопаснее. На практике такая привычка приносит больше проблем, чем ожидают.

Распространённый пример — SameSite=Strict для сессионных cookie. Это блокирует некоторые кросс‑сайтовые запросы, но может ломать обычные флоу, например ссылки для входа по email, переходы OAuth и возвраты с платёжных провайдеров. Пользователь кликает ссылку, попадает в приложение и вдруг оказывается разлогинен.

Область действия cookie создаёт тихую проблему. Команды ставят домен cookie как .example.com ради удобства, и тогда каждый поддомен получает эту cookie. Если забытый поддомен имеет более слабую защиту, поверхность раскрытия растёт без нужды. Держите домен как можно уже и не делитесь сессионными cookie по поддоменам без реальной необходимости.

CSP тоже подводит многих. Включают строгую политику в блокирующем режиме, не зная, что реально загружается в приложении. В результате ломаются собственные скрипты, исчезают шрифты, перестаёт работать аналитика или API‑вызовы. CSP эффективна, когда вы начинаете с отчётов, смотрите, что браузер сигналит, и ужесточаете правила шаг за шагом.

Ещё одна ошибка — собирать заголовки как наклейки на ноутбук. Копируют старые рекомендации, добавляют каждый найденный заголовок и никогда не чистят. В результате остаётся мешанина с устаревшими элементами, создающими шум без пользы. Если команда не может объяснить, зачем нужен заголовок — уберите или пересмотрите его.

Небольшая проверка здравого смысла помогает:

  • Протестируйте вход по email‑ссылке
  • Протестируйте OAuth и возвраты с оплаты
  • Проверьте, какие поддомены получают сессионные cookie
  • Запустите CSP в режиме отчётов перед принудительным применением
  • Убедитесь, что локальный и staging‑сервисы ведут себя как прод

Последний пункт часто упускают. Безопасность ломается в локальной и staging‑среде, и разработчики обходят её, чтобы двигаться дальше. Тогда production становится первым реальным тестом. Если cookie или CSP работают только в проде — развертывание уже хрупко.

Быстрые проверки перед релизом

Get a CTO Sanity Check
Ask Oleg to spot risky browser settings and rollout mistakes in your SaaS app.

День релиза — плохое время узнавать, что вход работает только в вашем обычном браузере или что сброс пароля падает при ужесточении правил cookie. Короткий прогон в чистом профиле браузера ловит многие такие вещи.

Начните с путей, которые люди проходят первыми. Создайте новый профиль без сохранённых cookie, расширений и с настройками по умолчанию. Пройдите регистрацию, вход, выход и сброс пароля от начала до конца. Если один шаг тихо зависит от старого состояния сессии — здесь это проявится.

Затем инспектируйте реальные ответы, а не только поведение приложения. Проверьте страницу входа, основной shell приложения после входа и несколько API‑ответов, важных для аутентификации. Вы хотите видеть те заголовки, которые собирались отправить, а не те, которые, как вы думали, добавляет фреймворк.

Короткий чеклист перед релизом:

  • Подтвердите, что сессионная cookie использует Secure и HttpOnly
  • Проверьте, что SameSite соответствует поддерживаемому флоу
  • Убедитесь, что домен и путь заданы намеренно, а не слишком широко
  • Удостоверьтесь, что время жизни и ротация соответствуют политике сессий
  • Сравните заголовки на страницах и в API, чтобы не было пробелов

Браузеры со строгими настройками конфиденциальности быстро ломают слабые предположения. Включите блокировку сторонних cookie и протестируйте основные пути снова, особенно редиректы входа, встроенные потоки и всё, что пересекает поддомены. То, что кажется рабочим в одном браузере, может упасть у реального клиента с жёсткими настройками.

CSP требует ещё одного взгляда. Просмотрите недавние отчёты перед релизом и отделите сигнал от шума. Если отчёт указывает на реальный inline‑скрипт, неожиданный домен или паттерн расширения браузера, который затрагивает многих пользователей — исправьте причину. Не глушите нарушения ради чистой панели мониторинга.

Этот финальный прогон должен казаться скучным. Если приложение работает в чистом браузере, cookie выглядят правильно, а заголовки согласованы по страницам и API — можно смело релизить.

Что делать дальше

Хорошие настройки помогают только если каждое новое сервис наследует их. Если команда поручает заголовки и флаги cookie отдельным разработчикам, те же ошибки вернутся с каждым новым приложением, поддоменом и потоком входа.

Напишите короткую политику и держите её простой. Страницы или две достаточно, если в них перечислены ожидаемые заголовки в публичных ответах, требуемые флаги cookie по умолчанию и несколько случаев, когда сервис может попросить исключение.

Простая база часто выглядит так:

  • Cookie используют Secure, HttpOnly и SameSite=Lax, если реальный кросс‑сайтовый флоу не требует иного
  • Браузерные приложения отправляют HSTS, X-Content-Type-Options: nosniff и явную Referrer-Policy
  • CSP начинается просто и ужесточается по мере удаления inline‑скриптов и лишнего стороннего кода
  • Команды документируют исключения в репозитории, а не в чьей‑то памяти

После этого сделайте правила тестируемыми. Добавьте проверки в CI, которые падают, когда сервис теряет требуемый заголовок или убирает флаг cookie. Это гораздо лучше, чем ловить проблему на позднем ревью. Если сервис входа выпускает сессионную cookie без HttpOnly, сборка должна остановиться сразу.

Рассматривайте изменения, касающиеся браузеров, уже на этапе проектирования, а не только на последней неделе перед релизом. Новый флоу аутентификации, клиентский портал на другом поддомене или сторонний виджет могут поменять требования к cookie и заголовкам. Эти решения легче исправить на диаграмме, чем в проде.

Здесь же внешняя экспертиза может помочь. Oleg Sotnikov на oleg.is работает как Fractional CTO и стартап‑советник, и такая проверка трезвого ума полезна, когда команда хочет практические рекомендации по настройкам безопасности приложений, инфраструктуре и workflow с AI. Это лучше всего работает, когда у команды уже есть черновая политика, CI‑проверка и один ответственный за поддержание дефолтов в сервисах.

Часто задаваемые вопросы

What flags should my session cookie use?

For a session cookie, set Secure, HttpOnly, and usually SameSite=Lax. Keep the lifetime short, and let your server expire old sessions instead of leaving them active for months.

Should every cookie use `HttpOnly`?

No. Put HttpOnly on cookies that hold login state or other secrets. Leave it off only when your front end must read the cookie, such as a theme or language setting, and keep those cookies separate from auth.

Is `SameSite=Lax` enough for most SaaS logins?

Most SaaS apps can start with SameSite=Lax. It blocks many cross-site requests while still allowing normal top-level navigation, which fits common login flows better than Strict.

When do I need `SameSite=None; Secure`?

Use SameSite=None; Secure only when a real cross-site flow needs it, such as some SSO setups, embedded apps, or payment returns. Test that path in real browsers first, because this setting opens the door wider than Lax.

Should I share auth cookies across subdomains?

Keep auth cookies on the narrowest domain and path you can. If your app lives on app.example.com, store the login cookie there unless you truly need to share it with another subdomain.

Which headers should I ship first?

Start with HSTS, Content-Security-Policy in report-only mode, frame-ancestors or X-Frame-Options, X-Content-Type-Options: nosniff, and a clear Referrer-Policy. That set blocks a lot of common problems without turning your config into clutter.

Should I enable HSTS right away?

Wait until HTTPS works on every host people can reach. Check old subdomains, admin tools, support pages, and redirects first, because HSTS tells the browser to keep using HTTPS even after a mistake.

How do I add CSP without breaking things?

Begin with Content-Security-Policy-Report-Only and watch what the page actually loads. Remove unused domains, fix inline scripts, and enforce the policy one step at a time so you can see what breaks.

Do I need frame protection on every page?

Protect login, billing, admin, and account pages first. Marketing pages sometimes need a video, demo, or scheduler embed, so give those pages their own rule instead of weakening the whole site.

What should I test before release?

Open a fresh browser profile and run signup, login, logout, password reset, billing return, and magic link flows from start to finish. Then inspect the real responses and confirm the cookie flags, scope, expiry, and headers match what you meant to ship.