Рядок у cookie, який важить як пароль
HTTP не памʼятає, хто ви. Після входу сервер створює сесію: дані лежать у нього, а браузер отримує лише у cookie й надсилає його з кожним запитом. Знати id означає бути цією людиною, поки сесія жива. Тому атаки тут три: вкрасти id (XSS, перехоплення по http, логи з cookie), підкинути свій () або змусити браузер надіслати cookie з чужого сайту (CSRF).
Фіксація працює так: атакувальник отримує від сайту справжній 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.
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 закривають решту сесій, коли змінюється хеш пароля.
Три кроки фіксації сесії
- Атакувальник створює id сесії a1b2….
- id сесії a1b2… використовує Жертва (підкинув).
- Дані йдуть від Жертва до Вхід на сайт.
- Вхід на сайт використовує Сесії (той самий id).
- Атакувальник надсилає подію запити з a1b2…, її отримує Сесії.
Новий id після входу
-
1
рядки 14–22
Те, що робить regenerate(true): дані переїжджають під новий випадковий id, а старий запис знищено. У чистому PHP це session_regenerate_id(true).
-
2
рядки 26–28
Новий id видається до того, як у сесії зʼявиться user_id. Laravel робить це в SessionGuard при кожному Auth::login.
-
3
рядки 35–41
Атакувальник тримає id, з яким жертва прийшла. Без regenerate цей id після входу стає залогіненим, з regenerate він веде в нікуди.
-
4
рядки 44–46
Вихід знищує сесію на сервері. Стара cookie з цим id більше нічого не відкриває, навіть якщо її хтось зберіг.
- Наведіть на пояснення, щоб підсвітити рядки.Торкніться пояснення, щоб підсвітити рядки.
Laravel керує сесією сам, крім цих місць
session(['user_id' => $user->id]);
Чому це дірка: Власний «вхід» записом у сесію обходить SessionGuard, і id не змінюється. Входьте через Auth::login чи Auth::attempt, а після них викликайте $request->session()->regenerate().
Auth::logout(); return redirect('/');
Чому це дірка: logout прибирає користувача, але дані сесії й id лишаються. Додайте $request->session()->invalidate() і regenerateToken(), інакше старий CSRF-токен і решта даних живуть далі.
'secure' => env('SESSION_SECURE_COOKIE')
Чому це дірка: Порожнє значення означає «як запит»: по https Symfony ставить Secure сам. За проксі застосунок має знати про https через trustProxies, інакше Secure не буде. У продакшені надійніше SESSION_SECURE_COOKIE=true.
'same_site' => 'none'
Чому це дірка: Cookie піде з будь-якого сайту, і браузер прийме її лише разом із Secure. Захист від CSRF тоді тримає лише токен. Беріть lax, а none лишайте для віджетів в iframe на чужих доменах.
SESSION_DOMAIN=.example.com
Чому це дірка: Cookie отримують усі піддомени, тож вразливий блог чи стенд на піддомені її прочитає. Без потреби лишайте null, тоді cookie належить лише одному хосту.
SESSION_DRIVER=cookie
Чому це дірка: Уся сесія лежить у зашифрованій cookie. Закрити її з сервера неможливо, вихід з інших пристроїв і список активних сесій не працюють. Для акаунтів беріть database або redis.
Auth::login($user, remember: true);
Чому це дірка: Cookie remember_web_… живе 400 днів і переживає закінчення сесії. Зміна пароля має змінити remember_token, а з AuthenticateSession у групі web старі cookie перестають діяти самі.
ini_set('session.use_trans_sid', '1');
Чому це дірка: id сесії потрапляє в посилання, Referer і логи, а фіксація стає тривіальною. У PHP 8.4 це застаріло. Лишайте use_only_cookies=1 і вмикайте use_strict_mode=1, щоб PHP не приймав id, якого сам не видавав.
framework: { session: { cookie_secure: auto, cookie_samesite: lax } }
Чому це дірка: Symfony після входу мігрує сесію сам: session_fixation_strategy у security.yaml типово migrate, не ставте none. cookie_secure: auto за проксі працює лише з правильними trusted_proxies.
Де тут проблеми із сесією
Натисніть на рядки, у яких бачите вразливість, і перевірте. Тут чотири проблеми.
Вставте у свій шаблон пул-реквесту
- Після входу, реєстрації й підтвердження пароля викликається $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.
Як цей сайт веде сесію
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.
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().- 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 недійсними.
Питання зі співбесід
Атакувальник підкидає жертві 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) перед небезпечними діями.