Сесії та токени: cookie, JWT, OAuth
Зміст
Форма логіну — двері, які застосунок відчиняє один раз. Далі вас упізнають не за паролем, а за одним рядком: значенням сесійної кукі або токеном у заголовку. Тому інциденти навколо доступу трапляються не на формі, а після неї, і тому питання «як ви тестуєте logout?» на співбесіді показове: з відповіді одразу видно, чи бачить кандидат серверний бік сесії.
Глава змішаного рівня. Перші пʼять розділів базові; розділи з позначкою поглиблення (JWT, , OAuth 2.0 і ) — senior-матеріал, який можна пропустити при першому проході й повернутися перед співбесідою на middle+. Механіка самої кукі й серверної сесії тут не переказується: її дім — глава Кукі, сесії та сховище браузера, а формат JWT і потік OAuth — глава Автентифікація та авторизація.
Життєвий цикл сесії: чотири моменти, у яких вона ламається
Веб-сесія (web session) — послідовність HTTP-запитів і відповідей, повʼязаних з одним користувачем. Ставку OWASP формулює без помʼякшень: ідентифікатор автентифікованої сесії тимчасово еквівалентний найсильнішому методу автентифікації застосунку, тобто його досить, щоб обійти і пароль, і другий фактор. Ведуть до захоплення пʼять шляхів: розкриття, перехоплення, передбачення, перебір і фіксація.
- Приймання ідентифікатора. Пермісивний механізм заводить сесію під будь-яке значення від користувача, суворий — лише під те, що згенерував сам. Вимога: ніколи не приймати ідентифікатор, якого не видавав, а отримавши такий — видати новий. Перевірка — підставити в сесійну кукі вигадане значення.
- Регенерація. Ідентифікатор
must be renewed or regeneratedпісля будь-якої зміни рівня привілеїв, і причина названа поіменно — запобігання фіксації. MDN каже те саме з боку кукі: перегенеровувати й надсилати сесійні кукі наново щоразу при автентифікації, навіть якщо вони вже існують. Інваріант тесту: значення до входу і після входу мусить відрізнятися. - Інвалідація. Гасити треба з обох боків, серверний названо найважливішим і обовʼязковим. Безпечне завершення складається з трьох частин: ручний вихід в інтерфейсі, завершення за бездіяльністю і належна інвалідація серверного стану — з окремим врахуванням випадку «користувач просто закрив вкладку».
Четвертий момент — , і їх три.
| Таймаут | Відлік | Що дає |
|---|---|---|
| idle | від останнього запиту сесії | закриває забуту відкриту сесію |
| absolute | від створення, попри активність | обмежує час користування захопленою сесією |
| renewal | періодично всередині живої сесії | скорочує вікно чинності значення |
Absolute — не дубль idle: захоплену сесію idle не спиняє, бо періодично генерує активність. Орієнтири джерела — 2–5 хвилин для високоцінних застосунків, 15–30 для низькоризикових, 4–8 годин для absolute; додає, що в банкінгу тримати неактивну сесію довше за 15 хвилин сенсу немає, а правильне значення взагалі є балансом безпеки й зручності. Головне: таймаут must be enforced server-side — «фронт розлогінює через 15 хвилин» перевіркою не є.
І окремо від чотирьох моментів — паралельні сесії, які є рішенням дизайну, а не дефектом. Чи дозволені кілька одночасних входів, вирішує застосунок; але рішення «не можна» зобовʼязує діяти: після нової автентифікації неявно завершити попередню сесію або спитати користувача. Список активних сесій із віддаленим завершенням — теж не зручність, а названий захід захисту на випадок загубленого пристрою чи підозри на крадіжку кукі.
Фіксація проти захоплення: дві атаки на той самий рядок
Джерело наполягає на розведенні: (session fixation) не є класом (session hijacking) — хоч і веде до того самого результату, тому й стоїть у переліку пʼяти шляхів вище. Захоплення краде встановлену сесію після входу; фіксація закріплює сесію в браузері жертви, тож починається до входу. Корінь фіксації — у застосунку: під час автентифікації він не призначає новий ідентифікатор.
Підсаджують трьома способами: ідентифікатором у посиланні, формою логіну від атакувальника і заголовком Set-Cookie. Механізм описаний у RFC: кукі не дають гарантій цілісності для сусідніх доменів, тож активний мережевий атакувальник впроваджує кукі, видавши себе за відповідь по HTTP, — HTTPS-сервер не відрізнить її від власної. MDN додає: кукі з Domain, поставлена з піддомену, доступна на всіх інших піддоменах. Основний контрзахід — регенерація (mandatory саме проти фіксації); додатковий — префікси імені (__Secure- вимагає атрибута Secure і захищеного , __Host- ще й Path=/ та відсутності Domain), але браузери без підтримки префіксів приймають такі кукі завжди, тож це , не заміна регенерації.
Захоплення лікується інакше. Крадіжка сесійної кукі має той самий вплив, що й крадіжка облікових даних, доки кукі не спливе; хоч би яким надійним був процес входу, проти неї він недостатній — MFA й стоять перед сесією. Причина: кукі приймають, не перевіряючи, хто їх надіслав, сервер перевіряє лише валідність значення. Крадуть їх зазвичай не із застосунку, а в користувача — шкідливим ПЗ чи фішингом, — тож єдиний хід сервісу — якнайшвидше виявити використання краденої кукі, зберігши інформацію про оточення при створенні сесії й порівнюючи її на кожному запиті. Обидві помилки методу задокументовані: зміна IP-Geo не доводить, а атака з тієї самої країни зміни не дасть узагалі. Найнадійніша реакція — повторна автентифікація з новою кукі, і ціна названа: надто часта повторна автентифікація псує досвід.
Прапорці cookie: що закриває кожен і чого він не закриває
Вихідна точка MDN: за замовчуванням усі значення кукі видимі кінцевому користувачеві й можуть бути ним змінені. Далі — рівно те, чого прапорці не роблять.
Secureзакриває канал, і тільки канал. Він захищає лише конфіденційність: активний мережевий атакувальник може перезаписатиSecure-кукі з незахищеного каналу, ламаючи цілісність, — підсадити кукі він не заважає. Обовʼязковим на сесійній куці однаково є: без нього атакувальник перенаправляє вихідний запит на HTTP, і браузер додасть кукі, навіть якщо сервер по HTTP не слухає.HttpOnlyзакриває читання, і тільки читання. Він не блокує XSS і не спиняє виконання скриптів; спроба прочитати таку кукі зі скрипта повертає порожній рядок. Але кукі зHttpOnlyвсе одно надсилається із запитами, ініційованими JavaScript, зокрема черезfetch(), тож упроваджений скрипт діє від імені користувача, не бачачи значення. Прапорці незалежні — на сесійній куці мають стояти обидва.Pathбезпекою не є: він не захищає від читання з іншого шляху й цілісності не дає, бо браузер приймає довільнийPathуSet-Cookie.
Три пастки, через які перевірка «прапорець є» нічого не доводить. Перша: рішення про надсилання ухвалює браузер, сервер лише оголошує атрибут — тож із завжди виставленим Secure сесії не працюють у середовищах по http, і це часта причина «на тесті логін не тримається», а не дефект застосунку. Друга: перевіряти треба відповідь, а не конфіг — при офлоуді TLS на балансер конфігураційне рішення не спрацює, а HttpOnly може, навпаки, дописати WAF, якщо правки коду неможливі. Третя, найпідступніша: браузер ігнорує нерозпізнані атрибути кукі, але не саму кукі — одруківка в назві прапорця (Secur, HttpOnl) нічого не ламає, вона мовчки лишає кукі без захисту, і видно це лише в заголовку Set-Cookie.
:2025 зводить вимоги в одне речення: серверний вбудований менеджер сесій, який видає новий ідентифікатор після логіну; ідентифікатор не в URL, у захищеній кукі, інвалідується після виходу, простою та абсолютного таймауту. Ознаки вразливості названі там же — ідентифікатор у URL чи прихованому полі й повторне його використання після логіну. Навіщо Secure, ілюструє категорія : атакувальник знижує зʼєднання з HTTPS до HTTP і краде сесійну кукі.
Зі сторінки прапорці не перевірити за побудовою: браузери блокують фронтенд-JavaScript від заголовка Set-Cookie (Fetch-специфікація робить його забороненим заголовком відповіді), а значення кукі не змінити власним заголовком Cookie через fetch(). ставлять на рівні або мережі; у Selenium робота з кукі — теж вбудована частина WebDriver-API, і набір доступних тесту полів задає специфікація, не сервер.
const [sid] = (await context.cookies()).filter((c) => c.name === 'sid');
expect(sid.httpOnly).toBe(true);
expect(sid.secure).toBe(true);
await context.addCookies([sid]); // цим же викликом повертають збережене значення після логауту
SameSite: наскільки міцний це рубіж
Дім теми — глава «CSRF і SameSite» цього розділу; тут лише те, що потрібно сесії. Атрибут обмежує область кукі так, що вона додається до запиту, лише якщо запит є same-site: Strict — тільки same-site, Lax — плюс крос-сайтові верхньорівневі навігації , None — усюди, і без Secure браузер таку кукі відкидає цілком.
Два факти, через які тест дає несподіваний результат. Перший: кукі без атрибута і кукі з явним Lax — різні сценарії; типова поведінка Chrome дозвільніша, бо пропускає частину кукі на верхньорівневих POST, і чернетка дозволяє цей режим лише для кукі без явного SameSite, обмежує його віком кукі (розумною межею названо дві хвилини) і прямо каже, що це тимчасовий перехідний захід. Другий: межа атрибута — сайт, а не походження. Same-site означає однакову схему й однаковий (registrable domain, у щоденній мові eTLD+1), тож кукі з app.example.com лишається same-site для всього в .example.com — і уразливий піддомен знімає захист. Схему додано в означення навмисно, щоб HTTP не був слабким каналом.
Сила заяви в джерел різна, і звести їх в одне речення не можна. Чернетка rfc6265bis: у суворому режимі й за підтримки клієнтом SameSite дає надійний захист від CSRF — із застереженням не робити цю позначку межею всього захисту сайту; про Lax та сама чернетка каже слабше — розумна глибина захисту проти атак на небезпечних методах, але не надійний захист від CSRF як категорії. OWASP: корисний як глибина захисту, але в більшості розгортань не замінює повноцінного CSRF-захисту. MDN: не повний захист, краще як доповнення до одного з інших. web.dev: ані Strict, ані Lax не є повним розвʼязанням безпеки сайту. Переказу «SameSite не захищає» не робить жодне з чотирьох.
Найчастіша причина провалу на практиці — стано-змінна операція, доступна через GET: Lax блокує лише небезпечні методи. І межа інструмента: читання атрибута з тесту дока Selenium обіцяє в Chrome 80+, Firefox 79+ і Selenium 4 — на старішому стенді «перевірка пройшла» означає «перевірки не було».
Тестування виходу: коли logout лише виглядає виходом
Що коротше живуть токени сесії, то менша ймовірність успішного захоплення, — тому тестують окремо. Дефект, заради якого тест існує, варто знати напамʼять: клієнтському токену ставлять нове значення, а серверний стан лишається активним і його можна перевикористати, повернувши сесійну кукі до попереднього значення. В інтерфейсі вихід при цьому виглядає бездоганно; гірший варіант WSTG називає окремо — користувачеві показують лише повідомлення, не роблячи нічого.
Процедура з двох кроків: зберегти значення кукі, якими впізнається сесія, і викликати вихід; якщо вихід підставив нові значення — повернути старі й перезавантажити сторінку із захищеної зони (технічно це context.addCookies() із прикладу вище). Очікуваний результат заданий джерелом: жодні дані, які має бачити лише автентифікований користувач, не мають бути видимі, а в ідеалі застосунок редіректить на публічну зону або форму входу. Що має статися з кукі, теж перевірне: сервер видаляє її явним заголовком — нова кукі з Expires у минулому або Max-Age, рівним нулю чи відʼємним, причому з тими самими імʼям, шляхом і доменом, інакше стара лишиться поруч; при конфлікті Max-Age має пріоритет. Але зникнення кукі доказом не є — саме тому крок із поверненням старого значення й існує.
Три перевірки добирають решту:
- Вихід за бездіяльністю. Люди закривають вкладку, не виходячи, тож сервер має завершити сесію сам; таймаут вимірюють серією запитів у захищену зону зі зростаючими паузами — коли зʼявиться поведінка виходу, приблизно дорівнює таймауту.
- SSO. Єдиний вхід часто породжує кілька сесій, які завершуються окремо; очікування сформульоване прямо — виклик виходу і в застосунку, і в самій системі SSO має спричинити глобальне завершення всіх сесій. Це два різні тести.
- Друга сесія того самого користувача. Чи гасить її вихід — питання до оголошеного рішення дизайну; кнопка виходу при цьому має бути доступна з кожної сторінки.
Якщо сесія тримається на токені, серверного запису, який можна знищити, немає: для JWT потрібне окреме рішення для інвалідації — зазвичай відкликаних токенів, — і тоді сесії вже не повністю stateless. Найдешевший контроль лишається той самий: короткий час життя токена. «Вихід» у токенній схемі часто означає «чекати exp», і це має бути свідомим рішенням продукту.
Поглиблення: JWT — що захищає підпис і де він ламається
Формат токена й сімейства алгоритмів — тема глави Автентифікація та авторизація; тут атакувальний кут. Підпис накриває і захищені заголовки, і claims, і блокує рівно одне: підробку токена або зміну claims у наявному — наприклад, зміну ролі з простого користувача на адміністратора. Звідси базова негативна перевірка: правимо роль у payload, токен має відхилятися.
Пастка перша — слабкий секрет симетричної схеми. При MAC той самий спільний секрет ділять емітент і аудиторія: ним і генерують, і перевіряють. Наслідок названо прямо — для кожної пари «емітент, аудиторія» потрібен різний секрет, інакше одна аудиторія може підробити токен, видаючи себе за емітента; у HS* перевіряльник — теж потенційний підробник. Асиметрична схема ваду знімає, бо публічний ключ може бути публічним. Вимога до секрету вимірювана: щонайменше такого ж розміру, як вихід алгоритму (256, 384 і 512 біт для HS256, HS384, HS512), і окремо — пароль як MAC-секрет не використовують. Читабельний рядок у конфізі — готова знахідка.
Пастка друга — alg: none. (Unsecured JWT) — передбачений специфікацією вид токена: alg зі значенням none і порожній рядок на місці підпису. Модальності різні й усі важливі: створити такий токен реалізація MAY; приймати без явного дозволу застосунку — MUST NOT; приймати за замовчуванням — MUST NOT; а щоб помʼякшити downgrade-атаки, згоду заборонено оголошувати глобально — її дають на окремий обʼєкт. Плюс окремий MUST одержувачу: перевірити, що значення підпису — порожня послідовність октетів, тож alg: none із непорожньою третьою частиною — окремий негативний випадок.
Чому пастка існує: підтримка none вимагається — сумісні реалізації зобовʼязані реалізувати лише HS256 і none. А алгоритм перевірки береться із самого токена: alg мусить бути присутній, зрозумілий реалізації й точно представляти вжитий алгоритм. Звузити перелік дозволених значень може тільки застосунок — це й шукають у коді, і це ж важіль проти ширшого класу, (algorithm substitution attack), у якій наявне значення підпису переграють під інший алгоритм.
Речення, яке варто цитувати на співбесіді: навіть якщо JWT успішно валідується, застосунок SHOULD відхилити його, коли вжиті алгоритми для нього неприйнятні. Валідність не дорівнює прийнятності; провал будь-якого кроку валідації означає, що токен MUST бути відхилений як невалідний ввід. OWASP зводить це до вимоги: парсер не має приймати alg: none, у недавніх реалізаціях це вимкнено за замовчуванням. Процедура перевірки механічна: узяти чинний токен, підмінити alg на none, спорожнити третю частину (самої підміни недосить), очікувати ; окремими кейсами — те саме з непорожнім підписом і токен, підписаний неочікуваним алгоритмом. Передають токен схемою Bearer у заголовку Authorization, і лише захищеним транспортом.
Поглиблення: час життя і refresh-токени
Refresh-токен (refresh token) — обліковка, потрібна лише щоб отримати новий токен доступу, коли поточний став невалідним або протермінувався. Дві деталі випадають із переказів найчастіше: видача не обовʼязкова (це рішення сервера авторизації на підставі оцінки ), а адресат один — refresh-токени призначені тільки для серверів авторизації й ніколи не надсилаються на сервери ресурсів, тож поява такого токена в запиті до API є знахідкою без додаткового аналізу.
Чому access-токен короткоживучий, сказано числом: сервери токенів SHOULD видавати короткоживучі bearer-токени — годину або менше, — особливо коли клієнт працює в браузері; строк повертає поле expires_in у секундах, і приклад специфікації 3600 означає годину. Самі refresh-токени MUST лишатися конфіденційними в передачі й у зберіганні та бути відомі лише серверу авторизації й тому клієнту, якому видані, а сервер MUST тримати привʼязку токена до клієнта. Для публічних клієнтів BCP додає: refresh-токен MUST бути або sender-constrained, або з ротацією.
Ротація — про виявлення, а не про зміну значення. Із кожною відповіддю оновлення сервер видає новий refresh-токен, попередній анулює, але звʼязок зберігає. Сенс у тому, що коли токен скомпрометовано і ним користуються обидва — атакувальник і легітимний клієнт, — хтось предʼявить анульований, і це повідомить сервер авторизації про злам. Тому «старий refresh-токен просто не працює» — недостатній результат тесту: питання в тому, що робить сервер, побачивши анульований токен.
Права токена доступу обмежують навмисно: він SHOULD бути обмежений аудиторією конкретного сервера ресурсів або невеликого їх набору. названі поіменно: token redirect — токен для одного сервера ресурсів приймає інший, помилково вважаючи його своїм; token replay — повторне використання вже вжитого токена. Коди відмови дають тесту : invalid_token (протермінований, відкликаний, зіпсований) — 401; insufficient_scope (токен чинний, прав бракує) — 403. І два місця, де токен не має зʼявлятися: bearer-токени SHOULD NOT передавати в URL сторінок, бо адреса з токеном із високою ймовірністю потрапить у логи, і їх MUST NOT зберігати в кукі, які можуть піти відкритим текстом, а хто зберігає токени в кукі — MUST ужити заходів проти CSRF. Відкликання в токенній схемі коштує статлесності: deny-list її забирає, а ключ deny-list не будують ні з сирого JWT, ні з його хешу — це дає обхід через malleability токена.
Поглиблення: OAuth 2.0, PKCE і OpenID Connect
Ролі й потік — тема тієї самої глави Автентифікація та авторизація. Безпековий кут тримається на трьох речах: адресі повернення, одноразовості коду й доказі володіння ним.
redirect_uri. Адреса MUST бути абсолютним URI й не містити фрагмента; для публічних клієнтів реєстрація обовʼязкова — без вимоги реєстрації атакувальник може вжити endpoint авторизації як , — і сервер SHOULD вимагати повну адресу. Значення MUST зіставлятися принаймні з однією зареєстрованою адресою, а при розбіжності сервер SHOULD повідомити власника ресурсу і MUST NOT автоматично редіректити на невалідну адресу. Про другий крок згадують рідше: на обміні коду redirect_uri має бути ідентичне тому, що було в запиті авторизації. BCP посилює правило до точного порівняння рядків (виняток — номери портів у localhost-редиректах нативних застосунків) і пояснює емпірично: зіставлення за шаблоном виявилося складнішим і помилковішим, а успішні атаки на нього спостерігались на реальних сервісах. Реєстрація шаблоном «будь-який піддомен» — знахідка, а не питання зручності. Код авторизації одноразовий і короткоживучий: повторний обмін має провалитися незалежно від .
PKCE (Proof Key for Code Exchange) закриває перехоплення коду в публічних клієнтів — тих, хто нездатний зберегти конфіденційність своїх облікових даних (SPA, нативні застосунки). Суть OWASP формулює одним реченням: крадений код без code_verifier на token endpoint обміняти не вдасться.
code_verifier — випадковий рядок від 43 до 128 символів; code_challenge рахують перетворенням plain або S256, і розбіжність на обміні дає MUST-помилку invalid_grant. Пастка дефолту готова до негативного тесту: code_challenge_method необовʼязковий і за відсутності дефолтиться в plain — пропущений параметр мовчки знижує захист. Клієнт, здатний на S256, MUST його вживати; plain SHOULD NOT застосовувати в новому коді; відкочуватися на plain після спроби S256 — прямий MUST NOT. З боку сервера захист від пониження описує OWASP: запит на токен із code_verifier приймається, лише якщо в запиті авторизації був code_challenge. Публічні клієнти MUST використовувати PKCE, конфіденційним він RECOMMENDED, а OWASP радить authorization code grant із PKCE для всіх типів клієнтів. Два гранти більше не застосовують: implicit (клієнти SHOULD NOT його вживати — токен тече через фрагмент URL, отже, через історію браузера, referrer і логи) і resource owner password credentials (MUST NOT — небезпечно віддає облікові дані клієнту). А CSRF на редирект- закриває будь-який із трьох механізмів: клієнти, які впевнилися в підтримці PKCE, MAY покластися на його захист; у потоках OpenID Connect той самий захист дає nonce; інакше обовʼязковим стає одноразовий state, привʼязаний до user agent. Тому «немає state — отже, дірка» автоматичним вердиктом не є.
OpenID Connect (OIDC) — простий шар ідентичності над OAuth 2.0: дає клієнту перевірити особу кінцевого користувача за автентифікацією, яку виконав сервер авторизації. Нових вузлів не зʼявляється — це ті самі сервер авторизації (OpenID Provider) і клієнт (Relying Party) з додатковим обовʼязком. Головне розширення, яке OIDC додає до OAuth 2.0, — ID Token, і це той самий JWT, тож усі пастки підпису застосовні без змін. Клейм aud MUST містити client_id тієї самої Relying Party, а після exp токен MUST NOT прийматися. Валідація описана чотирма вимогами: iss мусить збігтися точно; токен MUST бути відхилений, якщо клієнта немає серед аудиторій або серед них є недовірені; підпис MUST валідуватися ключами емітента; а якщо в запиті надсилали nonce, однойменний клейм MUST бути присутній і звірений, і його SHOULD перевіряти на replay. Звідси набір перевірок: чужий ID Token, зайва аудиторія, підмінений iss, протермінований токен, повторений nonce; окремо транспорт — звернення до UserInfo MUST іти через TLS. Чужого провайдера тестувати не треба: предметом є те, як ваш застосунок обробляє повернутий код і токен, тож провайдера на рівні мережі.
Типові помилки
«Кукі після логауту зникла — вихід працює». Виглядає як успішний вихід, а насправді клієнтське значення могло змінитися при живому серверному стані. Доказ дає лише повернення старої кукі й запит у захищену зону.
«Стоїть HttpOnly, значить XSS не страшний». Виглядає як захист від крадіжки, а насправді не блокує XSS: скрипт не прочитає значення, але кукі поїде з fetch()-запитом, і атака діє від імені користувача.
«У конфізі прапорець є». Виглядає як доказ, а насправді доводить лише Set-Cookie у відповіді: TLS може офлоудитись на балансер, WAF — навпаки дописати прапорець, а одруківка в назві атрибута кукі не ламає, бо браузер ігнорує нерозпізнаний атрибут.
«У потоці OAuth немає state — це дірка». Виглядає як знахідка, а насправді CSRF на редирект-ендпоінті закриває будь-який із трьох: PKCE, nonce або state.
«Токен пройшов валідацію — доступ коректний». Виглядає як зелений шлях, а насправді валідність не дорівнює прийнятності: токен, підписаний алгоритмом, якого застосунок не очікує, має бути відхилений, навіть якщо підпис сходиться.
Підсумок
- Ідентифікатор встановленої сесії тимчасово еквівалентний найсильнішому методу автентифікації. Усе, що після логіну, коштує стільки ж, скільки пароль.
- Фіксацію ловить один інваріант: значення сесійного ідентифікатора до входу і після має відрізнятися — і на будь-якій зміні привілеїв, не лише на логіні.
- Вихід і таймаут перевіряються повз інтерфейс. Повернуте старе значення кукі — єдиний доказ серверної інвалідації; таймаут рахує сервер.
- Кожен прапорець закриває рівно один канал:
Secure— конфіденційність каналу,HttpOnly— читання зі скрипта,SameSite— частину крос-сайтових запитів. Проти фіксації не працює жоден із них — її закриває регенерація ідентифікатора; аSameSiteжодне з чотирьох канонічних джерел не називає ані повним захистом, ані пустопорожнім. - У токенній схемі відкликання коштує статлесності. Тому найдешевшим контролем лишається короткий час життя токена — і саме він визначає, що насправді означає «вийти».
Можливі питання
«Чим фіксація сесії відрізняється від захоплення?» Дивляться, чи розумієте ви момент атаки: у фіксації ідентифікатор підсаджують до входу й він відомий атакувальнику з початку, у захопленні його крадуть після. Перше лікується регенерацією, друге — виявленням і скороченням часу життя.
«Як ви перевіряєте logout?» Питання-фільтр. Очікують не «натиснув і побачив форму входу», а процедуру: зняти значення сесійної кукі, вийти, повернути старе значення й звернутися в захищену зону повз інтерфейс. Плюс окремо SSO і друга сесія.
«Що робить HttpOnly і чого він не робить?» Перевіряють, чи не вивчили ви прапорець гаслом. Сильна відповідь називає межу: він закриває читання зі скрипта (спроба дає порожній рядок), але не спиняє XSS і не заважає кукі поїхати з fetch()-запитом.
«Що таке alg: none і чому це працює?» Слухають, чи знаєте ви, що це передбачений специфікацією вид токена з порожнім підписом, що реалізації зобовʼязані підтримувати none, і що алгоритм перевірки береться із самого токена — тому перелік дозволених значень мусить звузити застосунок.
«Навіщо PKCE, якщо є state?» Дивляться, чи не плутаєте ви дві задачі: state — про CSRF на редирект-ендпоінті, PKCE — про перехоплений код авторизації. І дрібниця, що виказує практику: пропущений code_challenge_method мовчки дефолтиться в plain.
«Чому в JWT складно зробити logout?» Хочуть почути розмін: підписаний токен перевіряють без звернення до сховища, тож дострокове відкликання потребує deny-list, а deny-list забирає ту саму статлесність, заради якої токен і брали. Звідси короткий exp як компроміс.
Джерела
Життєвий цикл сесії: чотири моменти, у яких вона ламається
- OWASP — Session Management Cheat Sheet — приймання ID, регенерація, три таймаути, інвалідація, паралельні сесії.
- OWASP WSTG — 4.6.6 Testing for Logout Functionality (WSTG-SESS-06) — складники завершення, приклад «банкінг — 15 хвилин».
- MDN — Using HTTP cookies — перегенерація сесійних кукі при кожній автентифікації.
- OWASP Cheat Sheet — User Privacy Protection — віддалена інвалідація сесій як захід захисту.
- RFC 6265 — HTTP State Management Mechanism — у кукі кладуть nonce, а не стан.
Фіксація проти захоплення: дві атаки на той самий рядок
- OWASP www-community — Session fixation — означення, три вектори, розведення з захопленням.
- RFC 6265 — HTTP State Management Mechanism — відсутність гарантій цілісності для сусідніх доменів.
- MDN — Using HTTP cookies —
Domainіз піддомену як механіка фіксації. - MDN — Set-Cookie — умови префіксів і межа їхньої дії.
- OWASP — Session Management Cheat Sheet — регенерація як обовʼязковий контрзахід.
- OWASP — Cookie Theft Mitigation Cheat Sheet — ціна краденої кукі, виявлення за оточенням, повторна автентифікація.
Прапорці cookie: що закриває кожен і чого він не закриває
- RFC 6265 — HTTP State Management Mechanism —
Secureлише про конфіденційність, незалежність прапорців,Pathне є безпекою, ігнорування нерозпізнаних атрибутів. - MDN — Using HTTP cookies — значення кукі видимі й змінні користувачем; заборона підміняти заголовок
Cookie. - MDN — Set-Cookie —
HttpOnly-кукі їде із запитами від JavaScript; заборонений заголовок відповіді. - OWASP www-community — HttpOnly — не блокує XSS, повертає порожній рядок, додається на WAF.
- OWASP www-community — Secure Cookie Attribute — рішення ухвалює браузер, пастка середовищ по http, офлоуд TLS.
- OWASP Top 10:2025 — Authentication Failures — вимоги до менеджера сесій і ознаки вразливості.
- OWASP Top 10:2025 — Cryptographic Failures — зниження HTTPS до HTTP і крадіжка сесійної кукі.
- Playwright — class BrowserContext — читання й підстановка кукі на рівні контексту.
- Selenium — Working with cookies — вбудовані методи WebDriver і фіксований набір полів.
SameSite: наскільки міцний це рубіж
- IETF Internet-Draft — Cookies: HTTP State Management Mechanism (rfc6265bis-22) — три значення, відкидання
NoneбезSecure, режим для кукі без атрибута, «robust defense in strict mode» і слабша заява проLax. - OWASP — CSRF Prevention Cheat Sheet — область реєстрованого домену, стано-змінний
GET, глибина захисту. - MDN — Cross-site request forgery (CSRF) — формула
Lax; «не повний захист, а доповнення». - web.dev — SameSite cookies explained — дефолт дозвільніший за явний
Lax; жодне значення не є повним розвʼязанням. - web.dev — "Same-site" and "same-origin" — same-site як однакова схема плюс однаковий eTLD+1.
- Selenium — Working with cookies — межа перевірки: Chrome 80+, Firefox 79+, Selenium 4.
Тестування виходу: коли logout лише виглядає виходом
- OWASP WSTG — 4.6.6 Testing for Logout Functionality (WSTG-SESS-06) — дефект «новий токен при живому серверному стані», процедура, очікуваний результат, вимірювання таймауту, SSO.
- OWASP — Session Management Cheat Sheet — обовʼязковість серверної інвалідації, доступність кнопки виходу, друга сесія.
- RFC 6265 — HTTP State Management Mechanism — видалення кукі заголовком із
Expiresу минулому. - MDN — Using HTTP cookies — збіг імені, шляху й домену при видаленні, пріоритет
Max-Age. - OWASP Cheat Sheet — JSON Web Token — deny-list, втрата статлесності, короткий час життя.
Поглиблення: JWT — що захищає підпис і де він ламається
- RFC 7519 — JSON Web Token (JWT) — означення незахищеного JWT, обовʼязковість
HS256іnone, відхилення токена, валідність проти прийнятності. - RFC 7518 — JSON Web Algorithms (JWA) — чотири модальності , порожня послідовність октетів, заборона глобального дозволу.
- RFC 7515 — JSON Web Signature (JWS) —
algобовʼязковий і точно представляє алгоритм; атака підміни алгоритму. - OWASP Cheat Sheet — JSON Web Token — що блокує підпис, секрет на пару «емітент, аудиторія», розмір секрету, заборона
alg: none. - RFC 6750 — The OAuth 2.0 Authorization Framework: Bearer Token Usage — схема
Bearerі захищений транспорт.
Поглиблення: час життя і refresh-токени
- RFC 6749 — The OAuth 2.0 Authorization Framework — означення refresh-токена, необовʼязковість видачі, заборона надсилати на сервери ресурсів,
expires_in, привʼязка до клієнта, сенс ротації. - RFC 9700 — Best Current Practice for OAuth 2.0 Security — sender-constrained або ротація, оцінка ризику, обмеження аудиторії.
- RFC 6750 — The OAuth 2.0 Authorization Framework: Bearer Token Usage — «година або менше»,
invalid_token/401,insufficient_scope/403, token redirect і replay, токен у URL і в кукі. - OWASP Cheat Sheet — JSON Web Token — deny-list, втрата статлесності, небезпечний ключ deny-list.
- OWASP Cheat Sheet — OAuth2 — мінімально потрібні права й привʼязка bearer-токена до єдиної аудиторії.
Поглиблення: OAuth 2.0, PKCE і OpenID Connect
- RFC 6749 — The OAuth 2.0 Authorization Framework — вимоги до
redirect_uriта їх звірка, поведінка при розбіжності, , публічний клієнт і відкритий редиректор. - RFC 7636 — Proof Key for Code Exchange (PKCE) — перехоплення коду, довжина
code_verifier,plainпротиS256, дефолтplain, заборона відкату,invalid_grant. - RFC 9700 — Best Current Practice for OAuth 2.0 Security — точне порівняння рядків та його підстава, PKCE для публічних клієнтів, заборона implicit і password credentials, три механізми CSRF-захисту.
- OWASP Cheat Sheet — OAuth2 — суть PKCE, захист від пониження, code grant для всіх клієнтів, витік токена через фрагмент URL.
- OpenID Connect Core 1.0 — шар ідентичності, ролі OP і RP, ID Token як JWT,
audіexp, чотири вимоги валідації, TLS для UserInfo. - Playwright — Mock APIs — мокання зовнішнього провайдера на рівні мережі.
Що таке веб-сесія і чому ідентифікатор автентифікованої сесії вважають настільки цінним?
Сесія — це те, що дозволяє серверу впізнавати користувача між окремими запитами, які самі по собі стану не мають: після входу сервер видає ідентифікатор, а браузер повертає його з кожним наступним зверненням. OWASP оцінює цей рядок без пом'якшень — поки сесія жива, він тимчасово вартий стільки ж, скільки найсильніший спосіб автентифікації в застосунку. Практично це означає, що маючи чуже значення сесії, не потребує ні пароля, ні другого фактора: він уже всередині. Тому MFA, і захищають лише двері, а все, що після них, тримається на одному значенні. Шляхів дістатися до нього кілька — розкриття, перехоплення, вгадування, перебір і фіксація, — і всі вони дають однаковий результат. Для тестувальника висновок прямий: перевірки навколо за пріоритетом стоять не нижче за перевірки самої форми логіну.
Що робить прапорець Secure і від чого він не рятує?
Secure каже браузеру не надсилати кукі незахищеним каналом — тобто закриває рівно одну властивість, конфіденційність передачі. Без нього атакувальнику достатньо змусити браузер піти на http-адресу того самого сайту: кукі поїде у відкритому вигляді, навіть якщо сервер по HTTP взагалі нічого не слухає. Саме на цьому будується класичний сценарій зниження з'єднання, який OWASP описує в категорії . Але цілісності не дає: активний мережевий атакувальник здатен перезаписати навіть Secure-кукі з незахищеного каналу, тож підсадити своє значення Secure не заважає. Плюс дуже практичний наслідок для стендів: рішення надсилати чи не надсилати ухвалює браузер, сервер лише оголошує атрибут — тому на середовищі без TLS сесія з Secure просто не тримається, і це конфігурація, а не дефект застосунку.
Що дає прапорець HttpOnly і де закінчується його зона відповідальності?
HttpOnly закриває доступ до значення кукі з клієнтського JavaScript — спроба прочитати таку кукі зі скрипта поверне порожній рядок. На цьому його межа й закінчується: він не блокує міжсайтовий скриптинг (cross-site scripting, XSS) і не заважає скриптам виконуватися. Найважливіша деталь, яку на співбесіді й перевіряють: кукі з HttpOnly однаково додається до запитів, які ініціює JavaScript, зокрема через fetch(). Тобто впроваджений скрипт значення не побачить, але цілком успішно виконає дії від імені користувача. Тому формулювання «в нас HttpOnly, значить XSS не страшний» — червоний прапорець у відповіді кандидата. Прапорець корисний і обов'язковий на сесійній куці, але як один шар, а не як лікування XSS; з Secure вони незалежні й мають стояти обидва.
Чому «прапорець виставлено в конфізі» ще не доказ, що він працює?
Тому що доказом є заголовок Set-Cookie у реальній відповіді, а не рядок у налаштуваннях. Між конфігом і відповіддю стоїть інфраструктура: при винесенні TLS на балансувальник застосунок може вважати з'єднання незахищеним і не поставити Secure, а WAF, навпаки, здатен дописати HttpOnly там, де правити код не можна. Ще підступніша пастка — одруківка в назві атрибута: браузер мовчки ігнорує нерозпізнаний атрибут, але саму кукі приймає, тож HttpOnl або Secur нічого не ламають, а просто лишають кукі беззахисною. Помітити це можна лише очима в сирому заголовку відповіді. Через це ставлять на відповідь сервера або на кукі рівня — зі сторінки прапорці не перевірити за побудовою, бо фронтендовому JavaScript заборонено читати Set-Cookie, а підмінити заголовок Cookie через fetch() не вийде.
Чим фіксація сесії відрізняється від захоплення сесії?
Різниця в моменті й у тому, звідки береться значення. При захопленні (session hijacking) атакувальник краде вже встановлену сесію після того, як користувач увійшов. При фіксації (session fixation) він навпаки заздалегідь підсаджує жертві відоме йому значення, чекає, поки та залогіниться під ним, і далі користується тією самою сесією — тобто атака починається до входу. OWASP наполягає, що фіксація не є різновидом захоплення, хоч результат однаковий. Корінь проблеми лежить у застосунку: він не призначає новий ідентифікатор під час автентифікації. Підсадити значення можна щонайменше трьома способами — посиланням із ідентифікатором, власною формою логіну і заголовком Set-Cookie, — і HTTPS сам по собі від цього не рятує, бо кукі не мають гарантій цілісності між сусідніми доменами. Лікуються атаки по-різному: фіксація — обов'язковою регенерацією ідентифікатора, захоплення — коротким часом життя сесії й виявленням підозрілого використання.
Чому сесійний ідентифікатор має змінюватися після входу і як це перевірити?
Бо це єдиний надійний контрзахід проти фіксації: якщо значення після автентифікації нове, підсаджене атакувальником стає марним. Вимога ширша за логін — оновлювати ідентифікатор треба на будь-якій зміні рівня привілеїв, наприклад при переході в адміністративний режим чи підвищенні ролі. З боку кукі те саме формулює MDN: сесійні кукі варто перегенеровувати й надсилати наново при кожній автентифікації, навіть якщо вони вже є. Тест на це простий і не потребує спеціальних інструментів: зняти значення сесійної кукі до входу, залогінитися, зняти знову — значення мусять відрізнятися. Друга половина перевірки — переконатися, що застосунок не заводить сесію під будь-яке значення ззовні: підставляємо у сесійну кукі вигадане й дивимось, чи сервер його прийме (не має) і чи видасть натомість власне.
Які бувають таймаути сесії і навіщо absolute, якщо вже є idle?
три, і кожен закриває свою прогалину. Idle рахується від останнього запиту в сесії й гасить забуту відкриту сесію на чужому чи незаблокованому пристрої. Absolute рахується від моменту створення сесії й спрацьовує незалежно від активності. Renewal періодично оновлює значення всередині живої сесії, скорочуючи вікно, у якому конкретний рядок чинний. Absolute не дублює idle саме тому, що захоплену сесію idle не завершить ніколи: атакувальник просто час від часу генерує активність і тримає її живою. Орієнтири джерел різняться за — від кількох хвилин для високоцінних застосунків до пів години для низькоризикових і кількох годин для absolute, а для банкінгу прямо каже, що тримати неактивну сесію довше за чверть години сенсу немає. Головна перевірка тут одна: таймаут мусить діяти на сервері, бо «фронт розлогінює через 15 хвилин» не заважає надіслати запит із тією самою кукі повз інтерфейс.
Як ви тестуєте logout?
Це питання-фільтр: відповідь «натиснув кнопку і побачив форму входу» показує, що кандидат бачить лише клієнтський бік. Дефект, заради якого тест існує, виглядає в інтерфейсі бездоганно — застосунок ставить нове значення сесійної кукі, а серверний запис лишає живим, тож стару сесію можна перевикористати. Тому процедура така: зняти значення кукі, якими впізнається сесія, викликати вихід, а потім повернути збережене значення в контекст браузера й звернутися в захищену зону — сторінкою або запитом до API. Очікуваний результат: жодних даних, доступних лише автентифікованому користувачеві, а в ідеалі — ще й редірект на публічну сторінку чи форму входу. Зникнення кукі з панелі DevTools нічого не підтверджує: воно описує стан браузера, а не серверний запис, і власне заради цього в процедурі й з'явився крок із поверненням значення. І це не один тест, а кілька: окремо вихід за бездіяльністю, окремо глобальне завершення при SSO, окремо доля другої сесії того самого користувача.
Чому сесійний ідентифікатор не має з'являтися в URL?
Бо адреса — найпублічніша частина запиту: вона осідає в історії браузера, у заголовку Referer та в логах серверів і , і достатньо одного чи пересланого посилання, щоб віддати доступ разом із ним. називає ідентифікатор в URL або прихованому полі прямою ознакою вразливого керування сесіями — поряд із повторним використанням того самого значення після входу. Те саме правило діє й для bearer-токенів: специфікація радить не передавати їх у URL сторінок саме через високу ймовірність потрапляння в логи. Для QA це дешева й дуже результативна перевірка: пройти основні сценарії й переглянути адресний рядок та мережеву панель на предмет значень, схожих на ідентифікатор чи токен. Знахідка тут не потребує складного доведення — сам факт наявності вже є дефектом.
Що робить SameSite і чи правда, що він «не захищає від CSRF»?
SameSite обмежує область кукі: Strict віддає її лише в межах того самого сайту, Lax додає до цього крос-сайтові верхньорівневі навігації , None знімає обмеження зовсім — і без Secure браузер таку кукі відкидає повністю. Ключове уточнення про межу: same-site рахується за схемою й , а не за , тому будь-які два піддомени example.com браузер вважає одним сайтом — і скомпрометований піддомен знімає захист із кукі всього домену. Формулювання «не захищає» — надто грубе спрощення: чернетка rfc6265bis називає суворий режим надійним захистом від CSRF, для Lax заява слабша, OWASP і MDN описують атрибут як , яка не замінює повноцінного CSRF-захисту, а web.dev каже, що жодне зі значень не є повним розв'язанням. Тобто правильна відповідь — не «захищає» і не «не захищає», а «залежить від значення і не є єдиним рубежем». Найчастіша практична діра при цьому банальна: стано-змінна операція, доступна через GET, бо Lax фільтрує лише небезпечні методи. Ще одна дрібниця, яку варто знати: кукі, у якої атрибут не оголошено взагалі, поводиться не так, як кукі з виписаним Lax, тож перевірка дефолту й перевірка явного значення — це два різні тести.
Кілька одночасних сесій одного користувача — це дефект?
Само по собі ні: чи можна входити з кількох пристроїв одночасно, вирішує продукт, і обидва варіанти легітимні. Дефектом стає неузгодженість — коли рішення «одночасні сесії заборонені» оголошене, але не реалізоване. Якщо заборонені, після нової автентифікації застосунок мусить або сам завершити попередню сесію, або принаймні спитати користувача, що робити. Якщо дозволені, з'являється інша обов'язкова частина: список активних сесій із можливістю завершити їх віддалено — OWASP зараховує його до заходів захисту, а не до зручностей, бо саме ним користуються, коли пристрій загубився або є підозра, що кукі вкрали. Тому перед тестом варто дізнатися оголошену поведінку, а вже потім перевіряти, чи збігається з нею фактична: вхід із другого контексту браузера й запит першою кукі відповідає на це за хвилину.
Що таке alg: none і чому реалізації взагалі його підтримують?
— це передбачений специфікацією різновид токена: у заголовку стоїть alg зі значенням none, а на місці підпису — порожній рядок. Підтримка не є недоглядом бібліотек: сумісні реалізації зобов'язані реалізувати мінімум HS256 і none. Небезпека виникає з іншого — алгоритм перевірки береться із самого токена, тобто з поля, яким керує той, хто токен надіслав. Специфікація розводить модальності: створити такий токен реалізація може, а от приймати його без явного дозволу застосунку або за замовчуванням заборонено, причому дозвіл не можна оголошувати глобально — лише для окремого об'єкта, щоб не відкривати шлях атакам зниження. Звузити перелік прийнятних алгоритмів здатен лише сам застосунок, і саме це шукають у коді. Негативний тест механічний: узяти чинний токен, поставити alg: none і спорожнити третю частину — самої підміни заголовка недостатньо, бо одержувач зобов'язаний перевірити, що підпис є порожньою послідовністю октетів. Окремими кейсами йдуть той самий none із непорожнім підписом і токен, підписаний алгоритмом, якого застосунок не очікує.
Чому спільний секрет у схемі HS256 — ризик, і які до нього вимоги?
У симетричній схемі той самий секрет і генерує підпис, і перевіряє його, тому кожен, хто здатен валідувати токен, здатен і підробити. Наслідок формулюють прямо: для кожної пари «емітент — аудиторія» потрібен окремий секрет, інакше одна аудиторія може випустити токен від імені емітента для іншої. Асиметрична схема цю ваду знімає за побудовою — публічний ключ можна роздавати без наслідків, підписати ним не вийде. Вимога до розміру вимірювана: секрет має бути не коротшим за вихід алгоритму, тобто 256, 384 і 512 біт для HS256, HS384 і HS512 відповідно. І окремо: паролі на роль такого секрету не беруть узагалі, тож будь-який читабельний рядок у налаштуваннях — привід одразу заводити дефект. Для QA це рідкісний випадок, коли перевірка робиться очима в конфігурації, а не запитом.
Токен успішно провалідувався. Цього досить, щоб пустити користувача?
Ні, і це одна з найкорисніших фраз для співбесіди: валідність не дорівнює прийнятності. Специфікація прямо каже, що навіть коли JWT валідується успішно, застосунок має його відхилити, якщо вжиті в ньому алгоритми для нього неприйнятні. Причина та сама, що й з alg: none: алгоритм оголошено всередині токена, тож без з боку застосунку перевірка «підпис сходиться» перевіряє не те. Ширший клас атак називають підміною алгоритму — коли наявне значення підпису переграють під інший алгоритм, ніж той, що мався на увазі. Практичний набір негативних перевірок звідси очевидний: токен, підписаний несподіваним алгоритмом; токен зі зміненими claims; токен із чужою аудиторією; протермінований токен. І дзеркальне правило: провал будь-якого кроку валідації означає , а не «пропустимо, бо решта збіглася».
Чому в токенній схемі складно зробити справжній вихід?
Тому що виходити нема звідки: підписаний токен перевіряється математично, без звернення до сховища, тож серверного запису, який можна було б знищити, просто не існує. Щоб відкликати токен достроково, доводиться заводити відкликаних значень і перевіряти його на кожному запиті — а це забирає ту саму статлесність, заради якої токени зазвичай і беруть. Є й тонкість реалізації: ключем deny-list не можна робити ні сирий JWT, ні його хеш, бо токен можна незначно змінити й обійти перевірку. Тому найдешевшим і найчастішим контролем лишається короткий час життя: специфікація радить видавати bearer-токени на годину або менше, особливо для браузерних клієнтів. Практичний висновок для тестування: у багатьох продуктах «вихід» насправді означає «дочекатися exp» — і команда має ухвалити це усвідомлено та записати у вимоги, а не дізнатися на демо.
Навіщо потрібен refresh-токен і що насправді дає його ротація?
існує лише для того, щоб отримати новий токен доступу, коли поточний протермінувався або став невалідним, — це дозволяє тримати access-токен коротким без постійних повторних логінів. Дві деталі випадають із переказів найчастіше. Перша: видавати його ніхто не зобов'язаний — сервер авторизації вирішує це сам, зваживши ризик. Друга: адресат у нього рівно один — сервер авторизації, і на сервери ресурсів такий токен не надсилають ніколи, тож знайдений у запиті до API refresh-токен є знахідкою без додаткових доказів. Ротація ж — не про те, що значення змінюється: з кожним оновленням сервер видає новий токен, анулює попередній, але зберігає зв'язок між ними. Сенс у виявленні: якщо токен вкрали і ним користуються двоє, хтось рано чи пізно пред'явить анульоване значення, і це сигналізує серверу авторизації про компрометацію. Тому результат тесту «старий refresh-токен більше не працює» неповний — питання в тому, що сервер робить, побачивши анульований токен.
Як за кодом відповіді відрізнити протермінований токен від нестачі прав?
Специфікація bearer-токенів дає тесту готовий . Помилка invalid_token — це протермінований, відкликаний або зіпсований токен, і супроводжує її код 401. Помилка insufficient_scope означає, що токен цілком чинний, але наданих ним прав для цієї операції бракує, і код тут 403. Різниця не педантична: 401 каже клієнту «оновися чи залогінься», 403 — «повторний вхід нічого не змінить». В автотестах це прямий асерт — і водночас місце, де контракт легко розходиться зі специфікацією: API віддає 403 на протермінований токен або 401 на брак прав. Окремо варто перевірити, що токен обмежений своєю аудиторією: токен, виданий для одного сервера ресурсів, інший приймати не має — інакше маємо той самий token redirect, який специфікація називає поіменно.
Навіщо PKCE, якщо вже є state?
Бо вони закривають різні задачі. state розв'язує CSRF на редирект-: одноразове значення, прив'язане до user agent, підтверджує, що повернення з провайдера відповідає запиту, який починав саме цей браузер. (Proof Key for Code Exchange) розв'язує інше — перехоплення коду авторизації в публічних клієнтів, тобто в SPA й нативних застосунків, які не здатні зберегти таємницю. Механіка: клієнт надсилає code_challenge у запиті авторизації й code_verifier на обміні, сервер звіряє їх і при розбіжності повертає invalid_grant — вкрадений сам по собі код без верифікатора нічого не дає. Дрібниця, яка одразу виказує практичний досвід: параметр code_challenge_method не обов'язковий і за відсутності дефолтиться в plain, тобто пропущений параметр мовчки знижує захист, тоді як клієнт, здатний на S256, зобов'язаний його вживати. І зворотний бік, який рятує від хибної знахідки: CSRF на редиректі закриває будь-який із трьох механізмів — PKCE, nonce в або state, — тому відсутність state автоматично дефектом не є.
Що перевіряють у redirect_uri і чому там наполягають на точному порівнянні рядків?
redirect_uri — це адреса, куди сервер авторизації повертає код, тож будь-яка слабкість у її звірці означає, що код поїде не туди. Базові вимоги: адреса має бути абсолютною і без фрагмента, для публічних клієнтів реєстрація обов'язкова (інакше endpoint авторизації перетворюється на ), адреса із запиту мусить збігтися бодай з однією зареєстрованою, а при невдалій звірці автоматичного редиректу на неї бути не повинно. Другу половину правила згадують рідше: коли клієнт міняє код на токени, він мусить надіслати ту саму адресу, яку вказував на кроці авторизації. Найкраща практика посилює це до порівняння рядків символ у символ — виняток лишили тільки для номерів портів у локальних редиректах нативних застосунків, — а підстава суто емпірична: шаблонна звірка на практиці виявилася заплутанішою й давала більше помилок, і атаки на неї фіксували на реальних сервісах. Тому зареєстрований шаблон на кшталт «будь-який піддомен» — це знахідка, а не питання зручності. Ще один дешевий тест поруч: код авторизації одноразовий і короткоживучий, тож повторний обмін тим самим кодом має провалитися, і це не залежить від наявності PKCE.
Що додає OpenID Connect до OAuth 2.0 і як тестувати логін через зовнішнього провайдера?
OpenID Connect — тонкий шар ідентичності над OAuth 2.0: він дозволяє клієнту дізнатися, хто саме автентифікувався, тоді як «чистий» OAuth відповідає лише на питання про делеговані права. Нових учасників не з'являється — ті самі сервер авторизації в ролі провайдера й клієнт у ролі Relying Party, — а головним доповненням є ID Token, і це звичайний JWT, тож усі пастки підпису переносяться сюди без змін. Валідація описана чітко: iss мусить збігтися точно, серед аудиторій має бути client_id клієнта й не має бути недовірених, підпис перевіряється ключами емітента, а надісланий nonce мусить повернутися й бути звіреним, бажано з перевіркою на повтор; після exp токен не приймають. Звідси й набір негативних кейсів: підмінений iss, зайва аудиторія в aud, ID Token від іншого клієнта, прострочений токен і повторно вжитий nonce. Найважливіше методологічне зауваження для автотестів: сам провайдер — не ваша зона відповідальності, перевіряти треба обробку коду й токена на своєму боці, тому провайдера підміняють на рівні мережі, а не клікають живий Google на кожному прогоні.
Три кейси, у яких сесію перевіряють повз інтерфейс: два інваріанти життєвого циклу (нове значення після входу і мертва сесія після виходу) у Playwright, аудит сирого заголовка Set-Cookie замість конфігурації, і набір негативних перевірок токена та потоку OAuth. Скрізь — що саме дивитися і чому на інтерфейс тут не працює.
Кейс 1. Два інваріанти сесії, які видно лише з боку кукі
Обидва дефекти цієї пари виглядають в інтерфейсі бездоганно. Перший: після логіну застосунок лишає той самий ідентифікатор — і будь-яке значення, підсаджене жертві до входу, стає робочим ключем. Другий: вихід ставить нове значення кукі, а серверний запис лишається живим, тож стару сесію можна повернути й користуватися далі. Ловляться обидва тим самим прийомом — знімком кукі до дії та після неї.
import { test, expect, type BrowserContext } from '@playwright/test';
const sidOf = async (context: BrowserContext) =>
(await context.cookies()).find((c) => c.name === 'sid')?.value;
test('після входу сервер видає новий ідентифікатор сесії', async ({ page, context }) => {
await page.goto('https://app.example.com/login');
const anonymous = await sidOf(context); // передлогінна сесія зазвичай уже є
await page.getByLabel('Email').fill('qa@example.com');
await page.getByLabel('Пароль').fill(process.env.QA_PASSWORD!);
await page.getByRole('button', { name: 'Увійти' }).click();
await expect(page.getByTestId('user-menu')).toBeVisible();
const authenticated = await sidOf(context);
expect(authenticated, 'сесійної кукі після логіну немає').toBeTruthy();
// якщо значення не змінилося — фіксація сесії проходить без жодних зусиль
expect(authenticated).not.toBe(anonymous);
});
Друга половина — вихід. Ключовий крок не «натиснути й побачити форму логіну», а повернути збережене значення й постукати в захищену зону повз UI.
test('logout інвалідує сесію на сервері, а не лише в браузері', async ({ page, context }) => {
await page.goto('https://app.example.com/dashboard'); // сюїта вже автентифікована
const stolen = (await context.cookies()).filter((c) => c.name === 'sid');
expect(stolen).not.toHaveLength(0);
await page.getByRole('button', { name: 'Вийти' }).click();
await expect(page).toHaveURL(/\/login/);
// «крадемо» власну ж кукі назад у той самий контекст
await context.addCookies(stolen);
const api = await context.request.get('https://app.example.com/api/me', { maxRedirects: 0 });
expect(api.status(), 'сервер прийняв стару сесію — вихід лише клієнтський').toBe(401);
await page.goto('https://app.example.com/dashboard');
await expect(page.getByTestId('user-menu')).toBeHidden();
});
Що дивитися і чому:
- Асерт на зникнення кукі нічого не доводить. Зниклий рядок у панелі Application означає лише те, що браузер більше не має значення; серверний запис при цьому може бути цілком живим. Доказом є відповідь сервера на повернене старе значення, і нічого дешевшого тут немає.
- Перевірка регенерації потрібна не лише на логіні. Той самий асерт має стояти на будь-якій — вхід у режим адміністратора, підтвердження другим фактором, зміна ролі. Саме там про регенерацію забувають найчастіше, бо «користувач уже залогінений».
context.requestділить сховище кукі з — тому запит до API летить із тими самими значеннями, що й сторінка, і окремо їх підставляти не треба. Це і зручність, і пастка: якщо у прикладі вище прибратиaddCookies, тест стане зеленим за неправильною причиною.- Idle- вимірюють, а не читають із вимог. Серія запитів у захищену зону зі зростаючими паузами (5, 10, 20 хвилин) показує, коли зʼявляється поведінка виходу; момент переходу приблизно й дорівнює таймауту.
- SSO — окремий тест, а не гілка цього. Вихід у застосунку і вихід у самій системі єдиного входу мають завершувати всі повʼязані сесії; перевіряти це доводиться двома різними сценаріями, бо ламаються вони теж окремо.
Кейс 2. Аудит Set-Cookie: читаємо відповідь, а не конфігурацію
Заявка «прапорці ми виставили» перевіряється рівно в одному місці — у сирому заголовку відповіді на логін. Знімаємо його curl-ом або з мережевої панелі й читаємо кожен рядок окремо.
HTTP/1.1 302 Found
Location: /dashboard
Set-Cookie: sid=Ky8sR2p...; Domain=.example.com; Path=/; HttpOnl; SameSite=Lax
Set-Cookie: csrf=8f21ab; Path=/; Secure; SameSite=Lax
Set-Cookie: ab_test=b; Path=/; SameSite=None
Set-Cookie: locale=uk; Domain=.example.com; Path=/; Max-Age=31536000
| Що бачимо | Що це насправді означає | Дія |
|---|---|---|
HttpOnl замість HttpOnly на sid | Браузер ігнорує нерозпізнаний атрибут, але кукі приймає — вона просто без захисту | Дефект; асерт на сирий рядок, а не на конфіг |
Немає Secure на sid | Значення поїде відкритим каналом при першому ж переході на http | Дефект на будь-якому середовищі з TLS |
Domain=.example.com на сесійній куці | Кукі стає доступною всім піддоменам, зокрема вразливим | Питання до дизайну; кандидат на __Host- |
SameSite=None без Secure на ab_test | Браузер відкидає кукі цілком: сервер її «ставить», а її немає | Дефект; типова причина «логін не тримається», якщо так поставлено сесійну кукі |
Той самий sid, що й у відповіді до логіну | Регенерації немає — фіксація працює | Дефект, найвищий пріоритет |
Рядки не складаються в один дефект: кукі, яку браузер відкидає цілком (None без Secure), решти атрибутів уже не має — тому одруківку й брак Secure шукають на куці, що зберігається.
В автотесті це асерт на рядок, а не на розібраний обʼєкт — саме тому, що одруківку в назві атрибута видно лише в тексті заголовка.
test('Set-Cookie сесії має повний набір атрибутів', async ({ request }) => {
const res = await request.post('https://app.example.com/api/login', {
data: { email: 'qa@example.com', password: process.env.QA_PASSWORD },
maxRedirects: 0,
});
const sid = res
.headersArray()
.filter((h) => h.name.toLowerCase() === 'set-cookie')
.map((h) => h.value)
.find((v) => v.startsWith('sid='));
expect(sid, 'сервер не поставив сесійну кукі').toBeTruthy();
expect(sid).toMatch(/;\s*Secure/i);
expect(sid).toMatch(/;\s*HttpOnly/i);
expect(sid).toMatch(/;\s*SameSite=(Lax|Strict)/i);
});
Що дивитися і чому:
- Розібрана кукі ховає причину. Об'єкт із
context.cookies()покажеhttpOnly: false, і тест почервоніє правильно — але не скаже, що атрибут насправді написаний з одруківкою. Сирий рядок економить пів години розслідування. - Той самий тест на стенді без TLS почервоніє законно. Якщо середовище працює по
http, кукі зSecureбраузер не поверне, і сесія не триматиметься — це конфігурація середовища, а не дефект застосунку. Такий асерт або прив'язують до середовищ із TLS, або запускають лише проти них. - Перевіряти треба відповідь того середовища, куди поїде реліз. При офлоуді TLS на балансувальник застосунок може вважати з'єднання незахищеним і не поставити
Secure, а WAF попереду здатен, навпаки, дописатиHttpOnly. Локальний конфіг у жодному з цих випадків правди не скаже. - Сесійну кукі й службові кукі оцінюють за різними мірками. Вимога
HttpOnlyдоcsrf-токена, який має читати фронтенд, або доlocale— це шум у баг-трекері: прапорці оцінюють за роллю кукі, і жорсткої вимоги вони варті саме там, де кукі несе автентифікацію.
Кейс 3. Негативні перевірки токена й потоку OAuth
Тут майже все робиться запитами до API, а зелений шлях доказом не є: цінність у тому, що застосунок відхиляє. Мінімальний набір випадків виглядає так.
| Кейс | Як зібрати | Очікуваний результат |
|---|---|---|
| Змінена роль у payload | Узяти чинний токен, підняти роль, лишити старий підпис | 401, підпис не сходиться |
| Незахищений токен | alg: none і порожня третя частина | 401, парсер не приймає |
Той самий alg: none із підписом | Непорожня третя частина при alg: none | 401, підпис мусить бути порожнім |
| Несподіваний алгоритм | Валідний підпис, але не той алгоритм, що очікує застосунок | 401, валідність не дорівнює прийнятності |
| Протермінований токен | exp у минулому | 401 з invalid_token |
| Бракує прав | Чинний токен без потрібного scope | 403 з insufficient_scope |
| Чужа аудиторія | Токен для іншого сервера ресурсів | 401, токен обмежений аудиторією |
| Повторний обмін коду | Двічі обміняти той самий код авторизації | Друга спроба провалюється |
| PKCE без верифікатора | Обмін коду без code_verifier | invalid_grant |
Найпоказовіші два кейси збираються в кілька рядків:
import { test, expect } from '@playwright/test';
const seg = (o: object) => Buffer.from(JSON.stringify(o)).toString('base64url');
test('незахищений токен не дає доступу', async ({ request }) => {
const header = seg({ alg: 'none', typ: 'JWT' });
const payload = seg({ sub: '42', role: 'admin', exp: Math.floor(Date.now() / 1e3) + 600 });
// порожня третя частина — саме такий вигляд має Unsecured JWT за специфікацією
const unsecured = `${header}.${payload}.`;
const res = await request.get('https://api.example.com/admin/users', {
headers: { Authorization: `Bearer ${unsecured}` },
});
expect(res.status()).toBe(401);
// окремий кейс: те саме, але з непорожнім підписом
const withGarbage = `${header}.${payload}.ZmFrZQ`;
const second = await request.get('https://api.example.com/admin/users', {
headers: { Authorization: `Bearer ${withGarbage}` },
});
expect(second.status()).toBe(401);
});
test('підняття ролі в чинному токені не проходить', async ({ request }) => {
const [h, p, s] = process.env.QA_TOKEN!.split('.');
const claims = JSON.parse(Buffer.from(p, 'base64url').toString());
const tampered = [h, seg({ ...claims, role: 'admin' }), s].join('.');
const res = await request.get('https://api.example.com/admin/users', {
headers: { Authorization: `Bearer ${tampered}` },
});
expect(res.status()).toBe(401);
});
Що дивитися і чому:
- Два кейси
alg: none— не дубль. Одержувач зобовʼязаний перевірити, що підпис є порожньою послідовністю октетів, тож реалізація може відхиляти один варіант і пропускати другий. Тест лише з порожньою третьою частиною цю дірку не побачить. 401проти403— це асерт, а не косметика. Протермінований, відкликаний чи зіпсований токен дає401, а чинний токен без потрібних прав —403. Переплутані коди самі по собі є дефектом контракту: клієнт по401піде оновлювати токен, а по403навіть не спробує.- у запиті до API — знахідка без обговорення. Його адресат — лише сервер авторизації; якщо він з'явився в заголовку до сервера ресурсів, це дефект незалежно від того, працює запит чи ні.
- Негативні варіанти підсовують мокуванням, а не живим провайдером. Playwright дозволяє підмінити відповідь зовнішнього сервісу на рівні мережі — саме так у відповідь кладуть підмінений
iss, чужу аудиторію чи повторенийnonce. Асерти при цьому стоять на тому, що з цією відповіддю робить ваш застосунок: чотири вимоги валідації ID Token задає Core. - Пропущений
code_challenge_method— окремий тест. Параметр не обовʼязковий і за відсутності означаєplain, тож захист тихо знижується без жодної помилки. Перевіряють це на рівні запиту авторизації, дивлячись, що клієнт реально надіслав.
Життєвий цикл сесії
- Можу пояснити, чому ідентифікатор живої сесії тимчасово вартий стільки ж, скільки найсильніший метод входу, і чому MFA цю проблему не закриває — вона стоїть до сесії, а не після.
- Тримаю в голові інваріант регенерації: значення сесійного ідентифікатора до входу і після входу різні, і так само на будь-якій , не лише на логіні. Плюс дзеркальна перевірка — застосунок не має заводити сесію під підставлене ззовні значення.
- Розрізняю три (idle, absolute, renewal), можу пояснити, чому захоплену сесію idle не спиняє, і памʼятаю, що відлік мусить бути серверним: розлогін на фронті перевіркою не є.
Фіксація проти захоплення
- Знаю різницю за моментом : фіксація підсаджує значення до входу, захоплення краде вже встановлену сесію після нього — і лікуються вони різним (регенерація проти короткого часу життя й виявлення).
- Можу назвати три вектори підсадки (ідентифікатор у посиланні, форма логіну від , заголовок
Set-Cookie), пояснити, чому HTTPS сам по собі від них не рятує, і що префікси__Secure-та__Host-— , а не заміна регенерації.
Прапорці cookie
- Знаю розподіл ролей:
Secureзакриває лише конфіденційність каналу,HttpOnly— лише читання зі скрипта,Pathбезпекою не є взагалі; прапорці незалежні, тож на сесійній куці потрібні обидва. - Можу пояснити межу
HttpOnly: скрипт значення не прочитає (дістане порожній рядок), але кукі однаково поїде зfetch()-запитом, тож упроваджений код діє від імені користувача наосліп. - Знаю три пастки перевірки — сесія не тримається на стенді без TLS через
Secure; конфіг розходиться з відповіддю при офлоуді TLS чи дописуванні прапорця на WAF; одруківка в назві атрибута мовчки лишає кукі без захисту — і тому ставлю на заголовокSet-Cookieабо на кукі рівня контексту, а не на налаштування.
SameSite
- Знаю формулу трьох значень, памʼятаю, що
NoneбезSecureбраузер відкидає повністю, і що межа атрибута — сайт (схема плюс ), а не : уразливий піддомен захист знімає. - Не плутаю кукі без атрибута з кукі з явним
Lax, вмію коректно сформулювати силу захисту (глибина захисту, не заміна CSRF-захисту) і знаю найчастішу практичну діру — стано-змінну операцію, доступну черезGET.
Вихід і таймаути
- Знаю процедуру перевірки виходу: зняти значення сесійних кукі, вийти, повернути старе значення в контекст і звернутися в захищену зону повз інтерфейс — і чому зникнення кукі з панелі DevTools доказом не є.
- Знаю, як сервер коректно прибирає кукі: надсилає її наново з
Expiresу минулому або з нульовим чи відʼємнимMax-Age, обовʼязково з тим самим імʼям, шляхом і доменом — інакше поруч зі старою просто зʼявиться друга. Коли обидва атрибути присутні, виграєMax-Age. - Памʼятаю про сусідні перевірки: idle вимірюю серією звернень до захищеного з дедалі довшими паузами, глобальне завершення при SSO виношу в окремий тест, а долю другої сесії того самого користувача звіряю з оголошеним дизайном.
JWT і час життя токенів (поглиблення)
- Знаю, що підпис блокує рівно одне — підробку токена й зміну claims у наявному, — і роблю з цього базовий негативний тест: правимо роль у payload, чекаємо .
- Можу пояснити
alg: none: стандарт описує такий токен окремо, підпис у ньому порожній, а підтримувати цей режим бібліотеки зобовʼязані — тож перелік прийнятних алгоритмів звужує лише сам застосунок. У тесті підміни заголовка мало — третю частину треба спорожнити. - Розрізняю валідність і прийнятність: токен із коректним підписом, зробленим неочікуваним алгоритмом, має бути відхилений. Знаю й вимоги до MAC-секрету — окремий на пару «емітент — аудиторія», не коротший за вихід алгоритму, і не пароль.
- Розумію ціну відкликання: забирає статлесність, а його ключем не можна робити сирий токен чи його хеш, тому базовим контролем лишається короткий час життя — година або менше для браузерних клієнтів.
OAuth 2.0, PKCE і OIDC (поглиблення)
- Знаю, що адресований лише серверу авторизації (у запиті до API це готова знахідка), що ротація існує заради виявлення компрометації, а не заради зміни значення, і що
401зinvalid_tokenта403зinsufficient_scopeдають тесту . - Знаю вимоги до
redirect_uri— обовʼязкова реєстрація для публічних клієнтів, точне порівняння рядків замість шаблонів, ідентичність адреси на обміні коду — і що сам код авторизації одноразовий, тож повторний обмін має провалитися. - Не плутаю задачі (перехоплений код) і
state(CSRF на редиректі), памʼятаю мовчазний дефолтplainуcode_challenge_methodі те, що ID Token — той самий JWT із чотирма вимогами валідації, а зовнішнього провайдера в автотестах , бо перевіряємо свій застосунок.
Квіз
Перед стартом
- Питань: 18
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
Що застосунок має зробити з сесійним ідентифікатором, якого сам не видавав?