Безопасные cookie: флаги HttpOnly, Secure и SameSite
Практика3 минРедакция Innovialabs
Коротко
Cookie сессии должна быть HttpOnly, Secure, с SameSite=Lax (или Strict), с префиксом __Host- и ограниченным сроком жизни на сервере. Эти флаги закрывают кражу через XSS, утечку по HTTP и большую часть CSRF.
Короткий ответ
Для cookie сессии в большинстве веб-приложений подходит такой набор:
Set-Cookie: __Host-session=abc123; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=86400
Каждый атрибут закрывает свою угрозу, и убрать любой из них — значит открыть конкретную дыру.
Что делает каждый флаг
| Флаг | Что делает | От чего защищает |
|---|---|---|
HttpOnly | cookie недоступна из JavaScript (document.cookie) | кража сессии через XSS |
Secure | cookie отправляется только по HTTPS | перехват в открытой сети, при случайном переходе на HTTP |
SameSite | ограничивает отправку при межсайтовых запросах | CSRF, часть утечек между сайтами |
Domain | расширяет cookie на поддомены | — (наоборот, расширяет поверхность атаки) |
Path | ограничивает путь | не является мерой безопасности |
Max-Age / Expires | срок жизни в браузере | долгоживущие украденные сессии |
HttpOnly не спасает от XSS как такового: вредоносный скрипт всё ещё может слать запросы от имени пользователя. Но он не сможет унести cookie и использовать её с другого устройства.
Domain лучше не указывать вовсе. Без него cookie становится host-only и уходит только на точный домен, который её выставил.
Lax, Strict и None
- Strict — cookie не уходит ни с одним межсайтовым запросом, даже при клике по ссылке из почты или мессенджера. Пользователь, пришедший по внешней ссылке, на первой странице выглядит разлогиненным.
- Lax — cookie отправляется при обычном переходе по ссылке (
GETверхнего уровня), но не с межсайтовымиPOST,fetch,iframeи картинками. Разумный выбор по умолчанию. - None — cookie уходит всегда. Нужна для виджетов в чужих
iframeи некоторых кросс-доменных сценариев. Обязательно вместе сSecure, иначе браузер её отвергнет.
Если SameSite не указан, разные браузеры ведут себя по-разному: часть применяет Lax по умолчанию, часть — нет. Всегда задавайте значение явно.
Частый компромисс: две cookie — одна Strict для опасных действий (смена пароля, платежи), другая Lax для обычной навигации.
Префиксы cookie
Браузер проверяет префиксы в имени и не примет cookie, которая не соответствует правилам:
__Secure-— cookie обязана иметьSecureи быть выставлена с HTTPS-страницы.__Host-— то же плюсPath=/и запрет наDomain. Такую cookie нельзя подменить с поддомена.
Для сессии __Host- — лучший вариант, если не нужен общий вход на поддоменах.
Время жизни сессии
- Cookie без
Max-AgeиExpires— сессионная, но браузеры с восстановлением вкладок могут хранить её днями. Не рассчитывайте на «закрыл браузер — вышел». - Главный срок жизни задаётся на сервере: idle-таймаут (сессия умирает при бездействии) и абсолютный таймаут (даже активная сессия заканчивается).
- При входе и повышении прав выдавайте новый идентификатор сессии — это закрывает session fixation.
- При выходе удаляйте сессию на сервере, а не только cookie в браузере.
- Для админок и финансовых разделов сроки короче, чем для обычного кабинета.
Ошибки, которые ломают вход или сливают сессии
Secureна локальном HTTP-окружении с нестандартным хостом — cookie не сохраняется, вход «молча» не работает. Используйте HTTPS локально илиlocalhost.SameSite=NoneбезSecure— cookie отбрасывается браузером.SameSite=Strictпри OAuth или платёжном редиректе: провайдер возвращает пользователя межсайтовым запросом, и состояние сессии теряется. Особенно часто ломаются возвраты черезPOST— они не получают cookie даже приLax.Domain=.example.comбез необходимости — cookie видят все поддомены, включая тестовые и сторонние сервисы.- Токен сессии в
localStorage— он доступен любому скрипту на странице. - Одинаковое имя cookie на разных поддоменах с разным
Path— браузер отправляет несколько значений, сервер берёт не то. - Бесконечные «запомнить меня» без отзыва токенов при смене пароля.
Справка по атрибутам — в документации MDN Set-Cookie.