Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Сессии и cookie: безопасное управление состоянием

Сессии и cookie: как хранить состояние пользователя и не потерять безопасность

Сессии: серверное и клиентское хранение, флаги cookie HttpOnly/Secure/SameSite, ротация идентификатора, отзыв сессий и практические настройки.

Прямой ответ

HTTP не хранит состояние, поэтому аутентифицированного пользователя приходится «запоминать» между запросами. Стандартный механизм — сессия: сервер создаёт запись с уникальным идентификатором, а браузер хранит этот идентификатор в cookie и подставляет в каждый запрос.

Где хранить состояние:

  • Серверная сессия. В базе или Redis лежат данные, в cookie — только случайный идентификатор. Преимущество: сессию можно отозвать мгновенно, данные не видны клиенту. Цена — общее хранилище для всех инстансов приложения.
  • Клиентское хранение (подписанный cookie или JWT). Данные живут у клиента, сервер проверяет подпись. Преимущество — отсутствие общего хранилища. Цена — отзыв невозможен без дополнительного списка, а содержимое читаемо, если не зашифровано. Механика токенов — в статье про refresh-токены.

Обязательные флаги cookie:

Флаг Что делает
HttpOnly Cookie недоступен из JavaScript — снижает ущерб от XSS
Secure Передача только по HTTPS
SameSite=Lax (или Strict) Ограничивает отправку cookie в межсайтовых запросах — защита от CSRF
Path=/ Область действия; сужение без необходимости ломает часть маршрутов
Domain Не задавать шире нужного: example.com отдаст cookie всем поддоменам
Max-Age / Expires Срок жизни; без них cookie живёт до закрытия браузера
response.set_cookie(
    "session_id", session_id,
    httponly=True, secure=True, samesite="lax",
    path="/", max_age=8 * 3600,
)

Ключевые правила:

  1. Идентификатор — криптографически случайный. Не менее 128 бит энтропии; счётчики и хеши от имени пользователя недопустимы.
  2. Ротация при входе. После успешной аутентификации идентификатор меняется. Это закрывает фиксацию сессии, когда атакующий заранее подсовывает свой идентификатор.
  3. Ротация при повышении прав. Смена пароля, подтверждение платежа, включение 2FA — повод выпустить новый идентификатор и завершить остальные сессии.
  4. Отзыв должен работать. Выход из системы, смена пароля и блокировка пользователя обязаны немедленно инвалидировать сессии.
  5. Ограничение времени. Абсолютный срок жизни сессии (например, 8–24 часа) и неактивности (15–60 минут для чувствительных операций).

Практика: хранение и отзыв

Серверная сессия в Redis:

import secrets

def create_session(user_id: int, ttl: int = 8 * 3600) -> str:
    sid = secrets.token_urlsafe(32)
    redis.setex(f"sess:{sid}", ttl, json.dumps({"uid": user_id, "auth_at": now()}))
    return sid

def revoke_all(user_id: int) -> None:
    for key in redis.scan_iter(f"sess:*"):
        data = json.loads(redis.get(key) or "{}")
        if data.get("uid") == user_id:
            redis.delete(key)

Для большого числа пользователей сканирование ключей непрактично — держите индекс «пользователь → список сессий» в отдельном наборе.

Таблица сессий в PostgreSQL удобна, когда нужен аудит: видно, откуда и когда создан сеанс, и его можно показать пользователю в списке активных устройств.

Рекомендации:

  1. Разделяйте сессию и «запомнить меня». Долгоживущий токен — отдельная сущность с отдельными правилами: одноразовый, привязанный к устройству, подлежащий ротации при каждом использовании.
  2. Не храните в cookie чувствительные данные. Ни паролей, ни полных платёжных реквизитов, ни персональных данных, если без этого можно обойтись.
  3. Проверяйте CSRF отдельно от SameSite. SameSite=Lax покрывает большинство сценариев, но для критичных операций нужен токен или двойная отправка — разбор в статье про CSRF и XSS.
  4. Настройте CORS осознанно. Access-Control-Allow-Credentials: true вместе с Origin: * — нерабочая и опасная комбинация; детали — в статье про CORS.
  5. Логируйте создание и отзыв сессий. Это часть журнала безопасности — см. audit log.

Практический сценарий: угон сессии через поддомен

Контекст: после компрометации одного поддомена (staging.example.com) злоумышленник получил доступ к сессиям пользователей основного сайта.

  1. Причина. Cookie ставился с Domain=.example.com, поэтому отправлялся на все поддомены, включая тестовый с устаревшим кодом и открытой регистрацией.
  2. Дополнительно. Флаг Secure не был выставлен, поэтому идентификатор сессии уходил по HTTP при первом переходе; HttpOnly отсутствовал, и скрипт на поддомене мог прочитать cookie.
  3. Исправление. (а) Убрали Domain, оставив cookie в пределах основного хоста; (б) выставили Secure, HttpOnly, SameSite=Lax; (в) включили HSTS, чтобы исключить первый незащищённый запрос; (г) отозвали все сессии и запросили повторный вход.
  4. Проверка. Проверили выдачу cookie на всех маршрутах и убедились, что на поддомены он не уходит; добавили тест, который падает при появлении cookie без требуемых флагов.
  5. Профилактика. Настройки cookie вынесли в одну функцию-фабрику, чтобы нельзя было выставить их вручную в отдельном обработчике; ротация идентификатора при входе включена по умолчанию.

Вывод: большинство инцидентов с сессиями — не следствие слабых алгоритмов, а следствие неверных флагов cookie и слишком широкой области действия.

FAQ

Что выбрать: серверную сессию или JWT? Для классического веб-приложения серверная сессия проще и безопаснее: мгновенный отзыв, отсутствие данных у клиента. JWT оправдан, когда нет общего хранилища или клиент не браузер (мобильное приложение, межсервисные вызовы), и тогда обязателен механизм отзыва через короткоживущие access-токены и ротацию refresh-токенов.

Почему нельзя хранить токен в localStorage? LocalStorage доступен из JavaScript, поэтому любая XSS-уязвимость приводит к краже токена. HttpOnly-cookie от скриптов закрыт. Обратная сторона — необходимость защиты от CSRF, которая решается SameSite и токеном.

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

Что делать с сессиями при горизонтальном масштабировании? Хранить состояние в общем хранилище (Redis, БД), а не в памяти процесса. «Прилипающие» сессии на балансировщике работают, но ломают graceful shutdown и усложняют деплой — см. healthcheck и graceful shutdown.

Источники: OWASP Session Management Cheat Sheet, MDN Set-Cookie, OWASP CSRF Cheat Sheet — ссылки в шапке.