<? phpukraine СПІВБЕСІДИ
Пошук по платформі
CORE PHP · JUNIOR ЧАСТО ПИТАЮТЬ

Як працюють сесії й cookies у PHP і що з цим робити в API?

У cookie браузер тримає лише ідентифікатор (типово `PHPSESSID`), а самі дані `$_SESSION` лежать на сервері: у файлах `session.save_path`, у Redis або у власному `SessionHandlerInterface`. Cookie сесії має бути `HttpOnly`, `Secure` і `SameSite`, а API замість сесії краще авторизувати токеном у заголовку `Authorization`, бо cookie шлеться браузером автоматично й тягне за собою CSRF і прив'язку до сервера.

Що саме відбувається після виклику `session_start()`?
Де фізично лежать дані сесії і що з них потрапляє в браузер?
Чому два AJAX-запити від одного користувача виконуються по черзі, а не паралельно?
Ми виносимо частину сайту в REST API. Чому там не варто тягнути сесії?
sessions cookies security HttpOnly SameSite CSRF API tokens

HTTP не пам'ятає попередній запит, тому стан доводиться відновлювати з того, що клієнт надсилає щоразу. session_start() шукає в запиті cookie з іменем session.name (типово PHPSESSID). Знайшов - читає за цим ідентифікатором файл зі сховища, десеріалізує його у $_SESSION, а в кінці запиту записує назад. Не знайшов - генерує новий ID і додає до відповіді заголовок Set-Cookie. У браузер їде лише рядок ідентифікатора; ані user_id, ані кошик, ані прапорці ролей у cookie не потрапляють. Тому півмегабайта в сесії ніяк не «роздуває трафік», зате роздуває сховище і час на серіалізацію кожного запиту.

Де саме лежать дані, вирішує session.save_handler. За замовчуванням це files, тобто файли sess_<id> у session.save_path. У файлового обробника є особливість, за яку його найчастіше лають: на час запиту він тримає ексклюзивне блокування файла. Поки одна вкладка вивантажує звіт, друга висить на session_start(). Рятує порядок дій: усі записи в $_SESSION робимо на початку, далі викликаємо session_write_close(), і блокування знято. Альтернативні обробники (redis, memcached, власний клас на SessionHandlerInterface) блокування або не ставлять, або ставлять за окремою опцією. Вони ж потрібні, коли серверів більше одного: файлова сесія прив'язана до конкретної машини, а користувач після балансування опиняється на іншій.

За безпеку сесійного cookie відповідають три атрибути, і кожен закриває свою загрозу. HttpOnly прибирає значення з document.cookie, тому XSS-скрипт уже не вичитає ідентифікатор одним рядком. Secure не дає надіслати cookie по HTTP, тобто прибирає перехоплення в дорозі. SameSite (доступний із PHP 7.3) керує тим, чи додасть браузер cookie до запиту з чужого сайту, і на шляху CSRF стоїть саме він, а не HttpOnly. Усі три задаються session_set_cookie_params() строго до session_start() або в php.ini через session.cookie_httponly, session.cookie_secure, session.cookie_samesite. Є ще пара налаштувань, про які часто забувають: session.use_strict_mode=1, щоб PHP не підхоплював ідентифікатор, якого сам не видавав, і session_regenerate_id(true) одразу після успішного логіну. Без них атака з фіксацією сесії робиться посиланням із заздалегідь підставленим ID.

Тепер про API. Сесія працює як неявний стан: браузер сам чіпляє cookie до кожного запиту на домен, а сервер по ньому відновлює контекст. Для класичного сайту це зручно, для API дає три різні проблеми. Автоматичне надсилання відкриває CSRF, тому кожен ендпоїнт, що змінює дані, тягне за собою токен і перевірку. Серверний стан прив'язує запит до сховища сесій, і будь-яке масштабування впирається в спільний Redis або в sticky sessions. І сам механізм не працює там, де немає браузера: мобільний застосунок, cron іншого сервісу, інтеграція партнера не мають ані cookie jar, ані спільного домену. Тому API авторизують явними обліковими даними в заголовку: Authorization: Bearer <token>, де токен - або непрозорий рядок, за яким у базі лежить запис із власником, правами й строком дії, або підписаний JWT, який перевіряється без походу в базу. У базі при цьому зберігають не сам токен, а його хеш, і звіряють через hash_equals(), точно як пароль.

Межу тут проводять по клієнту, а не по ярлику «сайт проти API». Якщо SPA живе на тому самому домені, cookie-сесія лишається кращим варіантом: HttpOnly робить токен недоступним для JS, чого localStorage не дає в принципі, а CSRF закривається SameSite плюс звичайним CSRF-токеном (у Laravel цей режим збирає Sanctum). Щойно з'являється сторонній домен, мобільний клієнт або сервіс-до-сервісу, cookie перестає бути зручним, і починається токен із коротким строком життя й механізмом оновлення. Змішувати обидва в одному ендпоїнті не варто: набір перевірок у них різний, і ендпоїнт, який приймає і сесію, і токен, зазвичай зрештою приймає щось третє.

declare(strict_types=1);

// Параметри cookie задаються ДО старту сесії, пізніше вони вже не діють
session_set_cookie_params([
    'lifetime' => 0,        // 0 = cookie живе до закриття браузера
    'path'     => '/',
    'domain'   => '',       // порожньо: поточний хост без піддоменів
    'secure'   => true,     // не піде по відкритому HTTP
    'httponly' => true,     // недосяжний з document.cookie
    'samesite' => 'Lax',    // атрибут доступний з PHP 7.3
]);
ini_set('session.use_strict_mode', '1'); // не приймати ID, якого сервер не видавав

session_start();            // читає PHPSESSID із запиту або створює новий

if ($isSuccessfulLogin) {
    // від фіксації сесії рятує саме новий ID на зміні привілеїв
    session_regenerate_id(true);   // true - видалити старий файл сесії
    $_SESSION['user_id'] = $user->id;
}

$_SESSION['last_seen'] = time();
session_write_close();      // знімає блокування файла: паралельні запити не чекатимуть

buildHeavyReport();         // довга робота вже поза блокуванням сесії

// --- Той самий користувач, але ендпоїнт API: жодного session_start() ---
$authorization = $_SERVER['HTTP_AUTHORIZATION'] ?? '';

if (! str_starts_with($authorization, 'Bearer ')) {
    http_response_code(401);
    header('WWW-Authenticate: Bearer');
    exit;
}

// у базі лежить лише хеш токена, порівняння - стійке до таймінг-атак
$tokenHash = hash('sha256', substr($authorization, 7));
$userId = findUserIdByTokenHash($tokenHash);  // всередині hash_equals()

if ($userId === null) {
    http_response_code(401);
    exit;
}
Що в cookie лежить тільки ідентифікатор, а дані живуть на сервері, тому розмір `$_SESSION` на трафік не впливає, зате впливає на сховище.
Що параметри cookie задаються до `session_start()` через `session_set_cookie_params()` або `session.cookie_*` у php.ini, а після старту сесії їх міняти вже пізно.
Що `HttpOnly` закриває доступ з JS і зменшує шкоду від XSS, `Secure` забороняє передачу по HTTP, а `SameSite` обмежує міжсайтові запити, і це різні загрози, а не три способи сказати одне й те саме.
Що після логіну потрібен `session_regenerate_id(true)` плюс `session.use_strict_mode=1`, інакше сесія вразлива до фіксації.
Що файловий обробник тримає ексклюзивне блокування файла сесії до кінця запиту, тому паралельні запити серіалізуються, і рятує `session_write_close()`.
Що для API стан у сесії ламає горизонтальне масштабування й додає CSRF-поверхню, а альтернатива - Bearer-токен (opaque або JWT), який клієнт передає явно.
Думати, що `$_SESSION` серіалізується в cookie й тому «не можна класти багато даних»: у cookie йде лише ID сесії.
Викликати `session_set_cookie_params()` або `ini_set('session.cookie_httponly', ...)` після `session_start()` і дивуватися, що нічого не змінилось.
Вважати `HttpOnly` захистом від CSRF: браузер і далі надсилає такий cookie з міжсайтовим запитом, просто JS його не бачить.
Ставити `SameSite=None` без `Secure`: браузери такий cookie відкидають.
Розраховувати, що сесія помре рівно через `session.gc_maxlifetime`: збирання сміття імовірнісне, а на Debian/Ubuntu воно взагалі винесене в системний таймер.
Тримати файлові сесії на кількох серверах за балансувальником і закривати проблему sticky sessions замість спільного сховища.
Читати `$_COOKIE['x']` одразу після `setcookie('x', ...)` у тому самому запиті: суперглобал заповнюється з запиту, а не з відповіді.
ПОРАДА

Скажіть одним реченням: «cookie - це квиток з номером, дані по номеру лежать на сервері». Далі назвіть три атрибути й що саме кожен закриває, і згадайте `session_write_close()` перед довгою операцією. Про API формулювання таке: сесія - це неявний стан, який браузер надсилає сам, а токен клієнт додає свідомо, і саме тому CSRF для нього не існує.

оновлено 8 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

`HttpOnly` прибирає cookie з `document.cookie` і тим зменшує шкоду від XSS. На автоматичне надсилання cookie він не впливає взагалі: міжсайтовий POST так само піде з сесійним ідентифікатором, тому CSRF закривають атрибутом `SameSite` і перевіркою токена.