Куки: маленькое состояние на стороне браузера

HTTP сам по себе не помнит пользователя между запросами. Куки — один из способов добавить это состояние: сервер отправляет заголовок Set-Cookie, браузер сохраняет пару «имя-значение» с атрибутами и затем автоматически прикладывает подходящие cookies к следующим запросам на тот же сайт. В PHP обычные cookies читаются через $_COOKIE, то есть они относятся к тому же внешнему вводу, что и данные из SAPI и суперглобалы.

setcookie() не записывает значение сразу в $_COOKIE. Она добавляет HTTP-заголовок к ответу, поэтому должна вызываться до вывода HTML, пробелов и любого тела ответа. Это та же практическая граница, что и у header() из HTTP-заголовки, ответы и редиректы.

<?php

declare(strict_types=1);

setcookie('theme', 'dark', [
    'expires' => time() + 60 * 60 * 24 * 30,
    'path' => '/',
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Lax',
]);

// В этом же запросе $_COOKIE['theme'] ещё может быть старым или отсутствовать.
// Новое значение браузер пришлёт только в следующем HTTP-запросе.

Удаление cookie — это тоже Set-Cookie, только с истёкшим сроком. Важно указать тот же path и domain, с которыми cookie была установлена, иначе браузер может удалить не ту запись или не удалить ничего.

<?php

setcookie('theme', '', [
    'expires' => time() - 3600,
    'path' => '/',
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Lax',
]);
Quick recall
Почему после `setcookie('theme', 'dark', ...)` нельзя рассчитывать, что `$_COOKIE['theme']` уже станет `dark` в этом же PHP-запросе?

Что делают Secure, HttpOnly и SameSite

Secure говорит браузеру отправлять cookie только по HTTPS. Для session ID это почти всегда обязательно: иначе идентификатор может уйти по незашифрованному HTTP-запросу.

HttpOnly запрещает доступ к cookie через document.cookie. Это снижает риск кражи session ID при XSS, но не делает XSS безопасным: вредный скрипт всё ещё может отправлять запросы от имени пользователя. Поэтому HttpOnly дополняет, а не заменяет XSS, экранирование вывода и шаблоны.

SameSite ограничивает отправку cookie в cross-site-запросах. Strict наиболее жёсткий, но может ломать переходы с внешних сайтов, OAuth или платёжные возвраты. Lax чаще подходит как рабочий баланс для обычной сессии. SameSite=None нужен для сценариев с third-party cookies, но современные браузеры требуют вместе с ним Secure. Даже с SameSite state-changing формы и запросы не стоит оставлять без токенов: это уже область CSRF и state-changing запросы.

flowchart TD A[Браузер получает Set-Cookie] --> B[Сохраняет cookie с атрибутами] B --> C[Следующий запрос к сайту] C --> D{Cookie подходит по domain/path/secure/samesite?} D -- да --> E[Браузер отправляет Cookie] D -- нет --> F[Cookie не прикладывается] E --> G[PHP читает ID из cookie] G --> H[Session storage на сервере] H --> I[$_SESSION с данными приложения]
flowchart TD
    A[Браузер получает Set-Cookie] --> B[Сохраняет cookie с атрибутами]
    B --> C[Следующий запрос к сайту]
    C --> D{Cookie подходит по domain/path/secure/samesite?}
    D -- да --> E[Браузер отправляет Cookie]
    D -- нет --> F[Cookie не прикладывается]
    E --> G[PHP читает ID из cookie]
    G --> H[Session storage на сервере]
    H --> I[$_SESSION с данными приложения]
Как cookie с session ID связывает браузер с серверным session storage.
Quick recall
Какой риск закрывает атрибут `Secure` у session cookie?

PHP sessions: ID в браузере, данные на сервере

PHP-сессия обычно состоит из двух частей. В браузере лежит только session ID, например cookie PHPSESSID=... или cookie с другим именем. На сервере по этому ID находится запись с данными: user_id, CSRF-токен, flash-сообщения, время последней активности, временные значения формы.

<?php

declare(strict_types=1);

session_name('id'); // лучше задать до session_start()
session_set_cookie_params([
    'lifetime' => 0,
    'path' => '/',
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Lax',
]);

session_start();

$_SESSION['user_id'] = 42;
$_SESSION['last_seen_at'] = time();

lifetime => 0 означает session cookie: браузер не должен сохранять её как постоянную cookie после закрытия браузерной сессии. Это не полноценный logout и не серверный timeout, но хороший дефолт для обычной авторизации. Для «запомнить меня» лучше делать отдельный long-lived token с ротацией и отзывом, а не растягивать жизнь основной PHP-сессии.

По умолчанию PHP часто хранит сессии в файлах на сервере, но в реальных проектах session handler может быть Redis, Memcached, база данных или хранилище фреймворка. Это важно для деплоя на несколько инстансов: если запросы пользователя попадают на разные серверы, все они должны видеть одно и то же session storage.

Quick recall
В PHP sessions где обычно лежит session ID, а где лежат данные вроде `user_id` и flash-сообщений?

Session fixation и session_regenerate_id()

Session fixation — атака, при которой злоумышленник пытается заранее навязать жертве известный session ID, а затем дождаться, пока жертва залогинится. После логина этот ID уже связан с аккаунтом.

Защита строится в два слоя. Первый — strict mode: PHP не должен принимать произвольный, ещё не созданный session ID от клиента. Второй — регенерация ID после изменения уровня привилегий: логин, смена роли, вход в admin-зону, иногда смена пароля или email.

<?php

declare(strict_types=1);

session_start();

function loginUser(int $userId): void
{
    // Пароль уже проверен через password_verify(); см. тему про хэши.
    session_regenerate_id(true);

    $_SESSION['user_id'] = $userId;
    $_SESSION['authenticated_at'] = time();
}

Аргумент true просит удалить старую сессионную запись. Для классического login handler это нормальная привычка. В очень нагруженных приложениях и при параллельных AJAX-запросах иногда нужна более аккуратная стратегия ротации, чтобы не порвать легитимные запросы пользователя, но принцип остаётся тем же: после повышения привилегий старый ID не должен продолжать открывать защищённую сессию.

Безопасные настройки в php.ini

Минимальный набор для обычного HTTPS-приложения выглядит так:

session.use_strict_mode = 1
session.use_cookies = 1
session.use_only_cookies = 1
session.use_trans_sid = 0

session.cookie_lifetime = 0
session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = Lax

session.use_only_cookies=1 и session.use_trans_sid=0 убирают передачу session ID через URL. Это важно: URL попадают в историю браузера, логи, referrer-заголовки, закладки и скриншоты. Session ID в URL почти всегда плохой знак.

https://example.com/account?PHPSESSID=known-session-id
GET /account?PHPSESSID=known-session-id HTTP/1.1
Referer: https://example.com/account?PHPSESSID=known-session-id

session.cookie_secure=1 включайте только на сайте, который реально работает по HTTPS. В локальной разработке без HTTPS сессия может «пропадать» на обычном HTTP-хосте или в отдельных браузерах, но localhost современные браузеры часто обрабатывают как исключение для Secure. Решение — локальный HTTPS или окруженческая настройка, но не отключение Secure в проде.

Logout, timeout и границы надёжности

session_destroy() удаляет серверные данные текущей сессии, но саму cookie браузера лучше погасить отдельно. Надёжный logout обычно очищает $_SESSION, уничтожает запись и отправляет истёкшую session cookie.

<?php

session_start();

$_SESSION = [];

$params = session_get_cookie_params();
setcookie(session_name(), '', [
    'expires' => time() - 3600,
    'path' => $params['path'],
    'domain' => $params['domain'],
    'secure' => $params['secure'],
    'httponly' => $params['httponly'],
    'samesite' => $params['samesite'] ?? 'Lax',
]);

session_destroy();

Сессия не доказывает личность пользователя сама по себе. Она только связывает запрос с серверной записью. Если session ID украден, угадан, зафиксирован или оставлен активным на чужом компьютере, сервер может принять атакующего за пользователя. Поэтому нужны HTTPS, регенерация ID, server-side timeouts, повторная аутентификация для рискованных действий и аккуратная конфигурация. Общая картина таких настроек разбирается в Конфигурация безопасности PHP.

Не храните в $_SESSION пароль, номер карты, большие объекты ORM и всё, что трудно инвалидировать. Обычно достаточно user_id, технических токенов, timestamps и небольшого состояния интерфейса. Пароли живут в базе как хэши через password_hash() и password_verify(), что относится к Пароли, хэши и криптография.

См. также

Sources

  1. PHP Manual: setcookie
  2. PHP Manual: session_regenerate_id
  3. PHP Manual: Runtime Configuration for Sessions
  4. PHP Manual: Securing Session INI Settings
  5. OWASP Cheat Sheet Series: Session Management
  6. MDN Web Docs: Set-Cookie header