Перейти к содержанию

Cookies в HTTP

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

В этом уроке выберем cookie для сессии редактора каталога. Результат — вы понимаете, как host, путь и атрибуты влияют на будущий запрос, а также умеете отличить cookie от содержимого самой сессии. Значения ниже условны; они не создавались генератором секретов и не пригодны для реального входа.

Сервер задаёт хранение

Считаем, что отдельный механизм входа уже успешно проверил Alice. Он пока не реализуется: мы не придумываем пароль или OAuth-процесс. Ответ после такой проверки может установить новый непрозрачный идентификатор:

HTTP/1.1 204 No Content
Set-Cookie: __Host-catalog_sid=sid-alice; Path=/; Secure; HttpOnly; SameSite=Lax
Cache-Control: no-store

Set-Cookie находится в ответе. Браузер сохраняет значение с его атрибутами и позже может добавить Cookie в запрос. В самом Cookie обычно нет исходных атрибутов Path, Secure или SameSite: они применяются браузером при выборе, а сервер получает пары имени и значения.

Префикс __Host- поддерживаемые браузеры связывают с Secure, Path=/ и отсутствием Domain. Он помогает ограничить создание cookie текущим узлом и общей областью пути. Это дополнительное правило имени, а не универсальная проверка прав: сервер всё равно должен валидировать сам идентификатор. Атрибуты и префиксы Set-Cookie.

Узел и путь

Мы не задаём Domain, поэтому cookie является host-only для catalog.example. Она не отправляется как cookie запроса к editor.catalog.example только потому, что узлы имеют общую часть имени. Если страница editor выполняет разрешённый запрос именно к catalog.example, целевой узел уже подходит, а остальные условия рассматриваются отдельно.

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

Для фактического обращения к личному профилю браузер может сформировать такой запрос:

GET /api/me HTTP/1.1
Host: catalog.example
Cookie: __Host-catalog_sid=sid-alice
Accept: application/json, application/problem+json

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

Secure и HttpOnly

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

HttpOnly запрещает обычному JavaScript читать cookie через document.cookie. Но браузер всё ещё может послать её с запросом, инициированным JavaScript. Следовательно, HttpOnly уменьшает одну возможность раскрытия, но не предотвращает любое действие вредного кода на том же origin. Если XSS получил возможность делать разрешённые обращения, граница задачи шире чтения одной строки.

Сессионный id не содержит роль editor или список доступных тем в доверенной форме. Это непрозрачная ссылка на серверную запись. Подмена его текста на admin=true не должна создавать администратора; сервер распознаёт только действующие случайные значения и связанный субъект. Содержимое такой записи разберём следующим уроком.

SameSite не равен same-origin

SameSite=Lax ограничивает часть межсайтовых передач cookie. В обычной модели Lax допускает некоторые безопасные верхнеуровневые переходы, но не является разрешением любого POST с другого сайта. При этом понятие site отличается от origin: оно учитывает схему и регистрируемый домен, а origin дополнительно различает узел и порт.

https://catalog.example и https://editor.catalog.example являются разными origin, но находятся в одном schemeful site. Поэтому SameSite не изолирует API от любого соседнего поддомена. Если в той же области работает недоверенная страница, одной cookie-политики недостаточно для защиты изменения. В уроке CSRF введём точный Origin и токен, привязанный к сессии.

SameSite=None потребовал бы Secure и разрешал бы более широкий контекст отправки по политике атрибута. Но браузер может независимо блокировать сторонние cookies. Нельзя обещать, что смена одного слова включит произвольный межсайтовый виджет во всех средах. Наш доверенный editor остаётся на том же site и не нуждается в расширении до None.

Время хранения и время доступа

В показанной cookie нет Max-Age и Expires. Обычно её относят к сессионному хранению браузера, однако восстановление браузерной сессии способно сохранить такое состояние. Поэтому закрытие окна не является надёжным правилом истечения серверного доступа.

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

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

Содержимое cookie остаётся недоверенным входом

Браузерные атрибуты помогают обычному браузеру выбрать передачу, но сервер не может считать любой Cookie доказательством их соблюдения. Другой HTTP-клиент способен прислать произвольную пару имени и значения. Поэтому запрос с правильным именем, но неизвестным id получает отказ, а не новую сессию с ролью по умолчанию.

Это также объясняет, почему нельзя хранить привилегию просто как cookie role=editor и доверять ей без проверки. Клиентское значение меняется вне нашей базы. В выбранной модели непрозрачный id только позволяет найти серверную запись, где действительность и права определены доверенной стороной. Отсутствие значения и непригодное значение могут вести к одинаковому предложению входа, но в диагностике остаются разными наблюдаемыми случаями.

Удаление означает две операции

Для выхода сервер отзывает свою запись и просит браузер удалить cookie с той же областью. Смысловой ответ после принятого logout:

HTTP/1.1 204 No Content
Set-Cookie: __Host-catalog_sid=; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=0
Cache-Control: no-store

Удаление браузерной строки не заменяет серверный отзыв. Если скопированный id остался у другого клиента, он должен стать недействительным после выхода именно из этой сессии. При этом выход с одного устройства не обещает отзыв всех остальных сессий Alice: такую функцию пришлось бы определить отдельно.

Нельзя объединять несколько Set-Cookie произвольной запятой как обычный список значений. Библиотека ответа должна поддерживать несколько полей корректно. Аналогично сервер не должен доверять первому попавшемуся одноимённому значению при неоднозначном входе: правила области и разбора должны быть согласованы.

Теперь вы можете объяснить, почему cookie не дошла: узел, путь, схема, site-контекст, credentials или самостоятельная браузерная политика. А когда она дошла, остаётся другой вопрос: существует ли действующая серверная сессия и что она разрешает? Именно его рассмотрим дальше, сохранив тот же идентификатор как условное обозначение.

Примеры продолжения серии. Оглавление курса.