PHP / Безопасность
Сессии, cookies и безопасный вход в PHP
Как настроить cookie сессии, проверить пароль, обновить хеш, сменить session ID, защитить POST через CSRF и корректно выйти.
Содержание статьи
Cookie хранится в браузере и отправляется серверу с подходящими запросами. PHP-сессия обычно хранит данные на сервере, а браузер получает только случайный идентификатор в cookie. Аутентификация отвечает на вопрос «кто пользователь», авторизация — «что ему разрешено».
Безопасный вход состоит не только из сравнения пароля: нужны HTTPS, защищённая cookie, смена идентификатора сессии, CSRF, ограничение попыток и проверка прав на каждом действии.
Настройка сессии
Параметры cookie задаются до session_start() и любого вывода:
<?phpdeclare(strict_types=1);ini_set('session.use_strict_mode', '1');ini_set('session.use_only_cookies', '1');session_name('app_session');session_set_cookie_params([ 'lifetime' => 0, 'path' => '/', 'domain' => '', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax',]);session_start();Secureотправляет cookie только по HTTPS;HttpOnlyзапрещает обычный доступ из JavaScript и уменьшает ущерб от части XSS;SameSite=Laxограничивает отправку в межсайтовых сценариях, но не заменяет CSRF-токен;use_strict_modeзапрещает принимать неизвестный идентификатор сессии.
Локальный HTTP требует отдельной development-конфигурации с secure => false. В production отключать Secure ради удобства нельзя.
Не запускайте сессию на каждой публичной странице без необходимости: cookie и Cache-Control могут ухудшить кеширование. Открывайте её там, где нужно серверное состояние.
Хранение пароля
При регистрации:
$passwordHash = password_hash($password, PASSWORD_DEFAULT);Не задавайте соль вручную: password_hash() создаёт безопасную случайную соль и включает параметры в результат. Столбец базы должен вмещать будущие форматы; VARCHAR(255) — практичный размер.
Перед хешированием проверьте разумную длину пароля в байтах и правила продукта. Не используйте SHA-256, MD5 или самодельное сочетание хешей для хранения паролей.
Получение данных входа
$email = is_string($_POST['email'] ?? null) ? trim($_POST['email']) : '';$password = is_string($_POST['password'] ?? null) ? $_POST['password'] : '';if (filter_var($email, FILTER_VALIDATE_EMAIL) === false || $password === '') { $loginError = 'Неверный email или пароль.';}Сообщение одинаково для неизвестного email и неверного пароля, чтобы не раскрывать наличие учётной записи.
До обработки POST проверьте CSRF-токен. Дополнительно ограничьте частоту попыток по учётной записи и другим надёжным сигналам; простая блокировка только по IP может затронуть общую сеть.
Поиск пользователя и проверка
$statement = $database->prepare( 'SELECT id, password_hash, is_active FROM users WHERE email_normalized = :email LIMIT 1');$statement->execute(['email' => mb_strtolower($email)]);$user = $statement->fetch();$dummyHash = DUMMY_PASSWORD_HASH;$storedHash = is_array($user) && is_string($user['password_hash'] ?? null) ? $user['password_hash'] : $dummyHash;$passwordIsValid = password_verify($password, $storedHash);$userIsActive = is_array($user) && (int) ($user['is_active'] ?? 0) === 1;if (!$passwordIsValid || !$userIsActive) { $loginError = 'Неверный email или пароль.';}DUMMY_PASSWORD_HASH — валидный хеш фиктивного пароля, созданный при настройке приложения и сохранённый в конфигурации. Он заставляет выполнить дорогую проверку даже для неизвестного email и уменьшает заметную разницу во времени ответа.
Нормализация email зависит от требований продукта. Домен регистронезависим, а локальную часть нельзя бездумно преобразовывать во всех системах; выбранное правило должно совпадать при регистрации, входе и уникальном индексе.
Успешный вход
if (!isset($loginError) && is_array($user)) { session_regenerate_id(true); $_SESSION['user_id'] = (int) $user['id']; $_SESSION['authenticated_at'] = time(); $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); if (password_needs_rehash($storedHash, PASSWORD_DEFAULT)) { $newHash = password_hash($password, PASSWORD_DEFAULT); $update = $database->prepare( 'UPDATE users SET password_hash = :password_hash WHERE id = :id' ); $update->execute([ 'password_hash' => $newHash, 'id' => (int) $user['id'], ]); } header('Location: /account', true, 303); exit;}session_regenerate_id(true) предотвращает сохранение заранее известного идентификатора после повышения привилегий. CSRF-токен также обновляется.
password_needs_rehash() позволяет перейти на новые параметры при следующем успешном входе.
Проверка пользователя и прав
function requireUserId(): int{ $userId = $_SESSION['user_id'] ?? null; if (!is_int($userId) || $userId < 1) { header('Location: /login', true, 303); exit; } return $userId;}Эта функция подтверждает только наличие ID в сессии. Перед редактированием статьи сервер проверяет, активен ли пользователь и имеет ли нужную роль или право на конкретный объект.
Нельзя доверять скрытому user_id, роли из формы или только отсутствию кнопки в интерфейсе.
CSRF для изменяющих действий
function requireValidCsrfToken(): void{ $sessionToken = $_SESSION['csrf_token'] ?? null; $requestToken = $_POST['csrf_token'] ?? null; if ( !is_string($sessionToken) || !is_string($requestToken) || !hash_equals($sessionToken, $requestToken) ) { http_response_code(403); exit('Не удалось проверить запрос.'); }}Вызывайте проверку до POST, PUT, PATCH и DELETE-действия. SameSite помогает, но не является единственной защитой.
Выход
Выход выполняется POST-запросом с CSRF, а не ссылкой GET:
requireValidCsrfToken();$_SESSION = [];if (ini_get('session.use_cookies')) { $params = session_get_cookie_params(); setcookie(session_name(), '', [ 'expires' => time() - 42000, 'path' => $params['path'], 'domain' => $params['domain'], 'secure' => $params['secure'], 'httponly' => $params['httponly'], 'samesite' => $params['samesite'], ]);}session_destroy();header('Location: /login', true, 303);exit;Параметры удаления должны совпадать с исходной cookie.
Обычная cookie без сессии
Для несекретного предпочтения:
setcookie('theme', 'dark', [ 'expires' => time() + 60 * 60 * 24 * 365, 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax',]);Cookie полностью контролируется клиентом. Не храните в ней роль, цену или право доступа без серверной проверки и подходящей криптографической схемы. Даже подписанная cookie может быть украдена, поэтому минимизируйте срок и объём.
Дополнительные меры
- rate limiting и журнал подозрительных входов;
- MFA для чувствительных ролей;
- повторная проверка пароля перед критичным действием;
- срок бездействия и абсолютный срок сессии;
- периодическая смена ID для долгих сессий без потери данных;
- CSP и экранирование против XSS;
- нейтральные публичные ошибки;
- отзыв активных сессий после смены пароля;
- проверка
Hostи redirect URL по allowlist.
Что запомнить
Хешируйте пароль через password_hash(), проверяйте через password_verify() и обновляйте через password_needs_rehash(). После входа меняйте session ID, защищайте все изменяющие запросы CSRF-токеном и проверяйте права на сервере. Cookie сессии требует HTTPS, Secure, HttpOnly и обоснованного SameSite.