Сессии и куки: как это работает на самом деле
Простой на вид вопрос, который быстро уходит вглубь: HTTP не помнит клиента — как же сайт узнаёт пользователя?
Сессия — это данные на стороне сервера, привязанные к идентификатору. Кука — небольшая строка на стороне клиента, которую браузер шлёт обратно с каждым запросом. Сессия обычно опирается на куку, но это разные вещи.
Вопрос 1: «Как работает session_start()?»
Что проверяет интервьюер
Понимаете ли вы, что в куке лежит только идентификатор, а сами данные — на сервере. Пошагово:
session_start()ищет идентификатор в кукеPHPSESSID(имя настраивается).- Если куки нет, PHP генерирует новый идентификатор и отдаёт его заголовком
Set-Cookie. - По идентификатору читается хранилище — по умолчанию файл вида
/var/lib/php/sessions/sess_<id>— и его содержимое попадает в$_SESSION. - В конце запроса массив сериализуется обратно в хранилище. Файл на это время блокируется.
<?php
session_set_cookie_params([
'lifetime' => 0, // до закрытия браузера
'path' => '/',
'secure' => true, // только по HTTPS
'httponly' => true, // недоступна из JavaScript
'samesite' => 'Lax', // защита от CSRF
]);
session_start();
$_SESSION['user_id'] = 42;
// закрыть запись пораньше, чтобы снять блокировку файла
session_write_close();
Про блокировку спрашивают отдельно: пока один запрос держит сессию открытой, параллельные запросы того же пользователя (например, десяток AJAX-вызовов) выстраиваются в очередь. Лечится вызовом session_write_close() сразу после того, как запись в $_SESSION больше не нужна, или переносом хранилища в Redis.
Вопрос 2: «Какие флаги должны стоять у куки авторизации?»
| HttpOnly | кука недоступна из JavaScript — украсть её через XSS уже нельзя |
| Secure | браузер шлёт куку только по HTTPS |
| SameSite=Lax | кука не отправляется при межсайтовых POST-запросах — базовая защита от CSRF |
| SameSite=Strict | строже: не отправляется даже при переходе по ссылке с другого сайта |
| Domain / Path | ограничивают область действия куки |
Сильный кандидат добавит: срок жизни сессии на сервере (session.gc_maxlifetime) и срок жизни куки — это разные настройки, и рассинхрон между ними даёт классический баг «вроде залогинен, но данных нет».
Вопрос 3: «Что такое session fixation и как от неё защититься?»
Атакующий заранее подсовывает жертве известный ему идентификатор сессии (через ссылку с параметром или через XSS), жертва входит в аккаунт — и сессия с уже известным id становится авторизованной. Защита в одну строку: менять идентификатор при смене уровня привилегий.
// после успешной проверки логина и пароля
session_regenerate_id(true); // true — удалить старый файл сессии
$_SESSION['user_id'] = $user->id;
$_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
// при выходе
$_SESSION = [];
session_destroy();
setcookie(session_name(), '', time() - 3600, '/');
Полезно упомянуть и session.use_strict_mode=1: с этой настройкой PHP отказывается принимать идентификатор, который он сам не выдавал.
Вопрос 4: «Как проверить, что кука не подделана?»
Если вы храните что-то в куке напрямую (например, «запомнить меня» или язык интерфейса), значение нужно подписывать. Подпись считают через HMAC с серверным секретом, а сравнивают константным по времени сравнением — обычное === для строк теоретически позволяет атаку по времени.
<?php
$secret = 'ключ-приложения';
$sessionId = 'a1b2c3';
$mac = hash_hmac('sha256', $sessionId, $secret);
echo substr($mac, 0, 16), "\n";
var_dump(hash_equals($mac, hash_hmac('sha256', $sessionId, $secret)));
var_dump(hash_equals($mac, hash_hmac('sha256', 'подделка', $secret)));
Вывод:
4fb499593966da78 bool(true) bool(false)
И главное правило: в куке не должно быть ни пароля, ни роли пользователя, ни «is_admin=1». Всё, что влияет на права, живёт на сервере — в сессии или в подписанном токене вроде JWT, подпись которого проверяется на каждом запросе.
Типичные ошибки кандидатов
- Говорят, что данные сессии хранятся в куке. В куке только идентификатор.
- Не знают про блокировку файла сессии и удивляются, почему параллельные AJAX-запросы «тормозят».
- Забывают
session_regenerate_id()после логина — прямая дорога к session fixation. - Ставят
SameSite=NoneбезSecure— браузер такую куку просто отбросит. - Пишут
session_start()после вывода HTML и получают ошибку «headers already sent»: куки — это заголовки, они идут до тела ответа. - Хранят роль пользователя в куке и удивляются, что её меняют в DevTools.
Как ответить кратко
«В куке
PHPSESSIDлежит только идентификатор, данные — на сервере: по умолчанию файл, на проде обычно Redis.session_start()читает хранилище в$_SESSIONи держит блокировку до конца запроса, поэтому запись стоит закрывать черезsession_write_close(). Куку авторизации выдаю сHttpOnly,SecureиSameSite=Lax, после успешного логина обязательно вызываюsession_regenerate_id(true)против session fixation, а права пользователя храню только на сервере».