<? phpukraine PHP-ПЛАТФОРМА
⌕Пошук по платформі
БЕЗПЕКА · КРИТИЧНІСТЬ: СЕРЕДНЯ OWASP 2025 A07 · Authentication Failures

Сесії й cookiesSessions and cookies

Для сервера ви це випадковий рядок у cookie. Хто має цей рядок, той і залогінений, тому атаки на сесію крадуть його, підкидають свій або змушують браузер надіслати його туди, куди не треба.

12 хв читання Junior → Senior
01 · ЩО ЦЕ

Рядок у cookie, який важить як пароль

HTTP не памʼятає, хто ви. Після входу сервер створює сесію: дані лежать у нього, а браузер отримує лише у cookie й надсилає його з кожним запитом. Знати id означає бути цією людиною, поки сесія жива. Тому атаки тут три: вкрасти id (XSS, перехоплення по http, логи з cookie), підкинути свій () або змусити браузер надіслати cookie з чужого сайту (CSRF).

СЛОВНИКId сесії. Випадковий рядок, за яким сервер знаходить дані сесії. У Laravel це 40 символів Str::random, у PHP за замовчуванням 32. Має бути непередбачуваним і жити лише в cookie, не в адресі.
СЛОВНИКФіксація сесії. Атака, коли жертва входить із id, який заздалегідь знає атакувальник. Якщо після входу id не змінився, атакувальник надсилає той самий id і стає жертвою.

Фіксація працює так: атакувальник отримує від сайту справжній id і підкидає його жертві через cookie з піддомену чи посилання на старий застосунок, що приймає id з адреси. Жертва входить, сервер пише user_id у ту саму сесію, і id атакувальника стає залогіненим. Захист один: після входу видати новий id. У PHP це session_regenerate_id(true), у Laravel $request->session()->regenerate(). Auth::login і Auth::attempt уже роблять regenerate(true) всередині SessionGuard, тож виклик у контролері страхує вхід, який іде повз guard, і заодно оновлює CSRF-токен.

Вихід має знищити сесію на сервері: Auth::logout() прибирає користувача й змінює remember_token, якщо він був, $request->session()->invalidate() стирає дані й видає новий id, regenerateToken() міняє CSRF-токен. Сама cookie нічого не вирішує: її можна зберегти й надіслати пізніше, тому значення має лише те, що сервер зробив із сесією.

Три прапорці cookie закривають три шляхи. не дає браузеру надіслати cookie по http, де її видно в мережі. ховає її від JavaScript, тож XSS не прочитає document.cookie, хоча діяти від імені людини на сторінці зможе. вирішує, чи йде cookie із запитом, який почався на іншому сайті: Lax пропускає звичайний перехід за посиланням і не пускає POST з чужої форми, fetch чи iframe, Strict не пускає навіть перехід, None пускає всюди й вимагає Secure. У Laravel це secure, http_only і same_site у config/session.php.

СЛОВНИКSecure. Прапорець cookie: браузер надсилає її лише по https. Без нього одна адреса http:// у мережі кавʼярні показує id сесії кожному, хто слухає.
СЛОВНИКHttpOnly. Прапорець cookie: JavaScript її не бачить, document.cookie її не містить. Захищає id від крадіжки через XSS, але не від дій скрипта в тій самій вкладці.
СЛОВНИКSameSite. Атрибут cookie зі значенням Strict, Lax або None. Каже браузеру, чи додавати cookie до запитів, що почалися на іншому сайті. Lax стоїть у Laravel за замовчуванням, і так само Chrome трактує cookie без атрибута.

lifetime у Laravel рахує хвилини без активності: кожен запит продовжує сесію, а абсолютної межі фреймворк не ставить. Драйвер file тримає сесії на диску одного сервера, database дає таблицю sessions з user_id і last_activity, тож можна показати активні сесії й закрити будь-яку, redis швидкий і сам видаляє прострочені ключі. Драйвер cookie кладе всі дані в зашифровану cookie, і закрити таку сесію на сервері неможливо. живе окремо: cookie remember_web_… на 400 днів, яку Laravel звіряє з remember_token у таблиці users. Auth::logoutOtherDevices($password) і middleware AuthenticateSession закривають решту сесій, коли змінюється хеш пароля.

СЛОВНИКRemember me. Довга cookie, з якою Laravel відновлює вхід після закінчення сесії. Містить id користувача, remember_token і хеш пароля, зашифровані APP_KEY. Зміна токена в базі робить усі такі cookie недійсними.
02 · ЯК ДУМАЄ АТАКУВАЛЬНИК

Три кроки фіксації сесії

ДІАГРАМА · УСЯ СХЕМА
Кроки: 3
отримав від сайтупідкинувпошта й парольтой самий idзапити з a1b2…Атакувальник:id сесії a1b2…ЖертваВхід на сайтСесії
Атакувальник:id сесії a1b2…ЖертваВхід на сайтСесії
Уся схема. Натисніть → або виберіть крок нижче, щоб пройти по ній частинами.
Клієнт Ключовий учасник Інфраструктура
  • Атакувальник створює id сесії a1b2….
  • id сесії a1b2… використовує Жертва (підкинув).
  • Дані йдуть від Жертва до Вхід на сайт.
  • Вхід на сайт використовує Сесії (той самий id).
  • Атакувальник надсилає подію запити з a1b2…, її отримує Сесії.
03 · ВРАЗЛИВИЙ І ЗАХИЩЕНИЙ КОД

Новий id після входу

PHP 8.5
1<?php
2
3session_start();
4$user = ['id' => 42, 'password' => password_hash('secret', PASSWORD_DEFAULT)];
5$before = session_id();
6
7// Було: вхід пише user_id у ту саму сесію, з якою людина прийшла.
8if (password_verify($_POST['password'] ?? 'secret', $user['password'])) {
9 $_SESSION['user_id'] = $user['id'];
10}
11
12echo $before === session_id() ? "id той самий, що до входу\n" : "id новий\n";
13
14// Було: вихід прибирає одне поле, а id сесії й решта даних живі.
15unset($_SESSION['user_id']);
1<?php
2
3// Сховище сесій: як storage/framework/sessions чи таблиця sessions.
4$sessions = [];
5
6function startSession(array &$sessions): string
7{
8 $id = bin2hex(random_bytes(20));
9 $sessions[$id] = [];
10
11 return $id;
12}
13
141// Як regenerate(true) у Laravel: дані під новим id, старий знищено.
15function regenerate(array &$sessions, string $id): string
16{
17 $new = bin2hex(random_bytes(20));
18 $sessions[$new] = $sessions[$id];
19 unset($sessions[$id]);
20
21 return $new;
22}
23
24function login(array &$sessions, string $id, int $userId, bool $regenerate): string
25{
262 if ($regenerate) {
27 $id = regenerate($sessions, $id);
28 }
29
30 $sessions[$id]['user_id'] = $userId;
31
32 return $id;
33}
34
353foreach ([false, true] as $regenerate) {
36 $planted = startSession($sessions); // id, який атакувальник отримав від сайту
37 $victim = login($sessions, $planted, 42, $regenerate); // жертва входить із цим id
38
39 $stolen = $sessions[$planted]['user_id'] ?? null;
40 echo $regenerate ? 'З regenerate: ' : 'Без regenerate: ';
41 echo $stolen === null ? "id атакувальника нічого не дає\n" : "id атакувальника тепер user {$stolen}\n";
42}
43
444// Вихід, як invalidate(): дані стерто, старого id на сервері більше немає.
45unset($sessions[$victim]);
46echo 'Після виходу: ', isset($sessions[$victim]) ? 'сесія жива' : 'сесії немає', "\n";
Перший запуск завантажить рушій PHP 8.5, приблизно 3,8 МБ Відкрити в пісочниці ↗
ВИВІД
Завантажуємо рушій PHP 8.5
Лише перший раз. Далі код запускається миттєво, рушій лишається в кеші браузера.
$ php main.php ▍
  1. 1 рядки 14–22

    Те, що робить regenerate(true): дані переїжджають під новий випадковий id, а старий запис знищено. У чистому PHP це session_regenerate_id(true).

  2. 2 рядки 26–28

    Новий id видається до того, як у сесії зʼявиться user_id. Laravel робить це в SessionGuard при кожному Auth::login.

  3. 3 рядки 35–41

    Атакувальник тримає id, з яким жертва прийшла. Без regenerate цей id після входу стає залогіненим, з regenerate він веде в нікуди.

  4. 4 рядки 44–46

    Вихід знищує сесію на сервері. Стара cookie з цим id більше нічого не відкриває, навіть якщо її хтось зберіг.

  5. Наведіть на пояснення, щоб підсвітити рядки.Торкніться пояснення, щоб підсвітити рядки.
04 · ПАСТКИ ФРЕЙМВОРКУ

Laravel керує сесією сам, крім цих місць

LARAVEL · ВХІД ПОВЗ GUARD
session(['user_id' => $user->id]);

Чому це дірка: Власний «вхід» записом у сесію обходить SessionGuard, і id не змінюється. Входьте через Auth::login чи Auth::attempt, а після них викликайте $request->session()->regenerate().

LARAVEL · ВИХІД
Auth::logout(); return redirect('/');

Чому це дірка: logout прибирає користувача, але дані сесії й id лишаються. Додайте $request->session()->invalidate() і regenerateToken(), інакше старий CSRF-токен і решта даних живуть далі.

LARAVEL · SECURE
'secure' => env('SESSION_SECURE_COOKIE')

Чому це дірка: Порожнє значення означає «як запит»: по https Symfony ставить Secure сам. За проксі застосунок має знати про https через trustProxies, інакше Secure не буде. У продакшені надійніше SESSION_SECURE_COOKIE=true.

LARAVEL · SAMESITE NONE
'same_site' => 'none'

Чому це дірка: Cookie піде з будь-якого сайту, і браузер прийме її лише разом із Secure. Захист від CSRF тоді тримає лише токен. Беріть lax, а none лишайте для віджетів в iframe на чужих доменах.

LARAVEL · DOMAIN
SESSION_DOMAIN=.example.com

Чому це дірка: Cookie отримують усі піддомени, тож вразливий блог чи стенд на піддомені її прочитає. Без потреби лишайте null, тоді cookie належить лише одному хосту.

LARAVEL · ДРАЙВЕР COOKIE
SESSION_DRIVER=cookie

Чому це дірка: Уся сесія лежить у зашифрованій cookie. Закрити її з сервера неможливо, вихід з інших пристроїв і список активних сесій не працюють. Для акаунтів беріть database або redis.

LARAVEL · REMEMBER ME
Auth::login($user, remember: true);

Чому це дірка: Cookie remember_web_… живе 400 днів і переживає закінчення сесії. Зміна пароля має змінити remember_token, а з AuthenticateSession у групі web старі cookie перестають діяти самі.

PHP · SID В АДРЕСІ
ini_set('session.use_trans_sid', '1');

Чому це дірка: id сесії потрапляє в посилання, Referer і логи, а фіксація стає тривіальною. У PHP 8.4 це застаріло. Лишайте use_only_cookies=1 і вмикайте use_strict_mode=1, щоб PHP не приймав id, якого сам не видавав.

SYMFONY · SECURITY
framework: { session: { cookie_secure: auto, cookie_samesite: lax } }

Чому це дірка: Symfony після входу мігрує сесію сам: session_fixation_strategy у security.yaml типово migrate, не ставте none. cookie_secure: auto за проксі працює лише з правильними trusted_proxies.

05 · ЗНАЙДИ ПРОБЛЕМУ

Де тут проблеми із сесією

Натисніть на рядки, у яких бачите вразливість, і перевірте. Тут чотири проблеми.

app/Http/Controllers/LoginController.php натисніть на підозрілі рядки
Тут усе гаразд: сам цей рядок не вразливий.
Тут усе гаразд: сам цей рядок не вразливий.
Тут усе гаразд: сам цей рядок не вразливий.
Тут усе гаразд: сам цей рядок не вразливий.
Для сесії тут усе гаразд. Текст помилки однаковий, хоча невідома пошта відповідає швидше, бо Hash::check не виконується; Auth::attempt вирівнює це через Timebox.
Тут усе гаразд: сам цей рядок не вразливий.
Тут усе гаразд: сам цей рядок не вразливий.
Тут усе гаразд: сам цей рядок не вразливий.
Пропущено. Так, тут фіксація. user_id пишеться в ту саму сесію без нового id. Потрібен Auth::login($user) і $request->session()->regenerate().
Пропущено. Теж проблема. id сесії ще раз лягає в cookie на рік і без HttpOnly, тож будь-який скрипт на сторінці його прочитає. Сесійна cookie Laravel уже є, дублювати її не треба.
Тут усе гаразд: сам цей рядок не вразливий.
Тут усе гаразд: intended веде лише на адресу цього сайту, яку запамʼятав middleware auth.
Тут усе гаразд: сам цей рядок не вразливий.
Тут усе гаразд: сам цей рядок не вразливий.
Тут усе гаразд: сам цей рядок не вразливий.
Тут усе гаразд: сам цей рядок не вразливий.
Пропущено. І тут. Вихід прибрав одне поле, а id сесії й решта даних живі. Потрібні Auth::logout(), invalidate() і regenerateToken().
Тут усе гаразд: сам цей рядок не вразливий.
Тут усе гаразд: сам цей рядок не вразливий.
Тут усе гаразд: сам цей рядок не вразливий.
Тут усе гаразд: сам цей рядок не вразливий.
Тут усе гаразд: сам цей рядок не вразливий.
Тут усе гаразд: сам цей рядок не вразливий.
Пропущено. Пароль змінено, а інші сесії й remember-cookie лишились робочими. Хто вкрав сесію, так у ній і сидить. Потрібен AuthenticateSession у групі web або Auth::logoutOtherDevices, і новий remember_token.
Тут усе гаразд: сам цей рядок не вразливий.
Тут усе гаразд: сам цей рядок не вразливий.
Тут усе гаразд: сам цей рядок не вразливий.
06 · ЧЕКЛІСТ ДЛЯ PR

Вставте у свій шаблон пул-реквесту

## Сесії й cookies
  • Після входу, реєстрації й підтвердження пароля викликається $request->session()->regenerate(), вхід іде лише через Auth::login чи Auth::attempt.
  • Вихід: Auth::logout(), $request->session()->invalidate() і regenerateToken().
  • У продакшені cookie сесії має Secure, HttpOnly і SameSite=Lax; SESSION_DOMAIN не ширший, ніж треба.
  • SESSION_LIFETIME відповідає ризику: кабінет з грошима чи персональними даними живе годинами, а не тижнями.
  • Драйвер database або redis, щоб сесію можна було закрити з сервера.
  • Зміна пароля закриває інші сесії й remember-cookie: AuthenticateSession у групі web або Auth::logoutOtherDevices.
  • id сесії не потрапляє в адреси, логи, JavaScript і відповіді API.
07 · ЯК У PHP UKRAINE

Як цей сайт веде сесію

config/session.php: драйвер database, тож кожна сесія це рядок у таблиці sessions; lifetime 120 хвилин без активності; http_only true; same_site lax; serialization json, тому дані сесії ніколи не проходять через unserialize. secure береться з SESSION_SECURE_COOKIE, а без неї Symfony ставить Secure сам для запиту по https. AuthenticateSession у групі web закриває інші сесії й remember-cookie, щойно змінюється пароль. Вхід і вихід живуть в AuthController.

php AuthController.php фрагмент
1 if (! Auth::attempt($credentials, $request->boolean('remember'))) {← remember me лише за галочкою
2 RateLimiter::hit($key, 60);
3
4 throw ValidationException::withMessages(['email' => 'Пошта або пароль не підходять.']);
5 }
6
7 RateLimiter::clear($key);
8 $request->session()->regenerate();← новий id після входу
9
10 return redirect()->to(self::landing($request, $request->user()));
11}
12
13public function logout(Request $request): RedirectResponse
14{
15 Auth::logout();← змінює remember_token
16 $request->session()->invalidate();← дані стерто, id новий
17 $request->session()->regenerateToken();← новий CSRF-токен
18
19 return redirect()->route('home');
20}
config/session.phpДрайвер database, lifetime 120, http_only true, same_site lax, serialization json.
bootstrap/app.phpAuthenticateSession додано в групу web: зміна пароля завершує всі інші сесії й remember-cookie.
app/Livewire/Developer/Register.phpПісля реєстрації Auth::login і одразу session()->regenerate(), так само в реєстрації компанії.
app/Http/Controllers/ProfileController.phpВидалення акаунта завершується Auth::logout, invalidate() і regenerateToken().
08 · СЛОВНИК
Id сесії
Випадковий рядок, за яким сервер знаходить дані сесії. У Laravel це 40 символів Str::random, у PHP за замовчуванням 32. Має бути непередбачуваним і жити лише в cookie, не в адресі.
Фіксація сесії
Атака, коли жертва входить із id, який заздалегідь знає атакувальник. Якщо після входу id не змінився, атакувальник надсилає той самий id і стає жертвою.
Secure
Прапорець cookie: браузер надсилає її лише по https. Без нього одна адреса http:// у мережі кавʼярні показує id сесії кожному, хто слухає.
HttpOnly
Прапорець cookie: JavaScript її не бачить, document.cookie її не містить. Захищає id від крадіжки через XSS, але не від дій скрипта в тій самій вкладці.
SameSite
Атрибут cookie зі значенням Strict, Lax або None. Каже браузеру, чи додавати cookie до запитів, що почалися на іншому сайті. Lax стоїть у Laravel за замовчуванням, і так само Chrome трактує cookie без атрибута.
Remember me
Довга cookie, з якою Laravel відновлює вхід після закінчення сесії. Містить id користувача, remember_token і хеш пароля, зашифровані APP_KEY. Зміна токена в базі робить усі такі cookie недійсними.
09 · ПЕРЕВІР СЕБЕ

Питання зі співбесід

Усі питання з безпеки →

Атакувальник підкидає жертві id сесії, який сам знає, і чекає, поки жертва увійде. Якщо після входу id той самий, атакувальник із цим id залогінений як жертва. Захист: новий id після входу, session_regenerate_id(true) у PHP чи $request->session()->regenerate() у Laravel.

Secure: cookie не йде по http, тож її не перехоплять у відкритій мережі. HttpOnly: JavaScript не прочитає cookie, тож XSS не вкраде id, хоча діяти в тій самій вкладці зможе. SameSite: cookie не додається до запитів, що почалися на іншому сайті; Lax блокує POST з чужої форми, fetch та iframe, але пропускає перехід за посиланням. Це головний барʼєр проти CSRF разом із токеном.

Cookie можна зберегти й надіслати знову, тож важить лише стан на сервері. Auth::logout прибирає користувача із сесії й змінює remember_token, але решта даних і id лишаються. invalidate() стирає дані й видає новий id, regenerateToken() міняє CSRF-токен. З драйвером cookie закрити сесію на сервері взагалі неможливо, бо її там немає.

У Laravel: Auth::logoutOtherDevices($password) перехешовує пароль, а AuthenticateSession у групі web порівнює хеш пароля в сесії з поточним і розлогінює решту; remember_token теж варто змінити. З драйвером database можна більше: таблиця sessions має user_id, ip_address, user_agent і last_activity, тож можна показати список пристроїв і видалити окремий рядок. Плюс короткий lifetime, абсолютний ліміт сесії власним middleware і повторне підтвердження пароля (password.confirm) перед небезпечними діями.