Куки: маленькое состояние на стороне браузера
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',
]);Что делают 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 с данными приложения]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.
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 = Laxsession.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-idsession.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(), что относится к Пароли, хэши и криптография.
См. также
- SAPI и суперглобалы — почему
$_COOKIEи$_SESSIONлучше держать на границе приложения. - GET, POST и фильтрация ввода — как отличать внешний ввод от уже нормализованных данных.
- HTTP-заголовки, ответы и редиректы — почему cookies отправляются до тела ответа.
- CSRF и state-changing запросы — почему
SameSiteне заменяет CSRF-токены. - XSS, экранирование вывода и шаблоны — какие риски остаются даже с
HttpOnly. - Конфигурация безопасности PHP — системные настройки PHP для production-среды.