vyvchy
    Теми розділу

    11 · Security для QA

    CSRF і SameSite

    Зміст

    Сесія тримається на одному рядку, який браузер підставляє сам: отримавши кукі від сайту, він додає її до будь-якого наступного запиту на цей сайт — байдуже, хто цей запит ініціював. Саме ця зручність робить логін одноразовим, і саме вона відкриває міжсайтову підробку запиту (cross-site request forgery, CSRF): чужа сторінка змушує браузер жертви виконати дію на довіреному сайті, а сервер бачить цілком валідну сесію.

    Для QA тема незручна тим, що обидва звичні вердикти тут хибні: «токен у формі лежить — захищено» і «стоїть SameSite=Lax — CSRF закрито». Перше доводить лише те, що значення передали, друге — залежить від того, чи є в застосунку мутація, досяжна . Тому й розмова про CSRF швидко зʼїжджає з назв на механізм.

    Означення самої та її місце у вебі — глава Автентифікація та авторизація; й прапорці кукі — Сесії та токени. Тут — захисний бік: за чим відбирають , як улаштований токен, що насправді дає SameSite, чим закривають ендпоінти без форм і як відтворити атаку так, щоб вердикт спирався не на враження.

    Три умови, за яких ендпоінт вразливий

    CSRF починається не з експлойта, а з фільтра: більшість ендпоінтів до цієї атаки байдужі. MDN формулює фільтр трьома умовами, які діють разом. Сайт: уживає HTTP-запити для зміни якогось стану на сервері; уживає лише кукі, щоб підтвердити, що запит прийшов від автентифікованого користувача; уживає в запиті лише параметри, які може передбачити.

    ні

    так

    ні

    так

    Запит змінює стан на сервері

    Упізнання лише за кукі

    Умова не виконана

    Усі параметри передбачувані

    Кандидат на перевірку CSRF

    ні

    так

    ні

    так

    Запит змінює стан на сервері

    Упізнання лише за кукі

    Умова не виконана

    Усі параметри передбачувані

    Кандидат на перевірку CSRF

    Друга умова вирішує суперечку про API. Питання не в тому, JSON там чи форма, а в тому, що саме сервер приймає як доказ особи в цьому запиті. Гайд тестування OWASP ставить його як аудит до будь-якого відтворення: якщо керування сесіями спирається лише на клієнтські значення — інформацію, доступну браузеру, — застосунок вразливий. Механізм один: браузер автоматично надсилає інформацію, що ідентифікує сесію, і, отримавши кукі від сайту, шле її з усіма подальшими запитами до нього. Кукі не єдиний носій — те саме стосується застосунків на самих лише механізмах HTTP-автентифікації. Звідси й вибір заходу: підхід обирають за клієнтом і методом автентифікації, а для тих, хто покладається на кукі, лишаються необхідними.

    І окремо — на чому стоїть вердикт. перелічує чотири CSRF: поведінка браузера щодо повʼязаної із сесією інформації; знання атакувальником валідних URL і функціональності; керування сесіями, що спирається лише на відоме браузеру; існування HTML-тегів, чия присутність спричиняє негайне звернення до ресурсу (img). Перші три суттєві для наявності вразливості, четвертий лише полегшує експлуатацію, тож «робочого експлойта немає» вердикту не скасовує.

    Анти-CSRF токен: що саме він ламає

    CSRF-токен (CSRF token) — непередбачуване значення, яке сервер вбудовує у видану сторінку; легітимна сторінка додає його до стано-змінного запиту, а сервер виконує запит лише при збігу. Чому це працює, видно з третьої умови вище: токен ламає саме передбачуваність параметрів — без нього атакувальник не може створити валідний запит до бекенду.

    Один із найпопулярніших і рекомендованих заходів — патерн синхронізованого токена (synchronizer token pattern), і нормативна частина в ньому не «вкласти токен», а звірити: серверний компонент мусить перевірити наявність і валідність токена в запиті та порівняти його з токеном, знайденим у сесії користувача. Вимоги до значення:

    • унікальне для сесії користувача, таємне, непередбачуване — велике випадкове значення від надійного генератора;
    • генерується на боці сервера — раз на сесію або на кожен запит;
    • не передається через кукі в синхронізованому патерні;
    • не витікає в серверні логи чи в URL.

    Останній пункт для QA найдешевший: токен у рядку запиту — знахідка сама по собі, без експлойта. Вибір «на сесію чи на запит» джерело подає як розмін: токени на запит безпечніші, бо вікно експлуатації вкраденого значення мінімальне, зате псують зручність — класична жертва тут навігація «назад».

    Захід добирають за архітектурою: застосунок зі станом — синхронізований токен; без стану — double submit cookies; сайт, керований API, який не може вживати теги <form>, — кастомні заголовки запиту. Перший крок — не писати своє, а перевірити, чи фреймворк уже дає CSRF-захист.

    Double submit улаштований так: сайт ставить псевдовипадкове значення кукі й вимагає те саме значення полем форми; запит валідний лише при збігу. Стоїть це на політиці одного — чужий сайт кукі не прочитає й не змінить. Але наївна форма має названий обхід: атакувальник, здатний писати кукі на цільовий домен (уразливий піддомен, захоплення DNS, кукі відкритим HTTP на кукі без префікса __Host-), підставить обидві половини сам. Робочий варіант — підписаний double submit, привʼязаний до автентифікованої сесії; підпису без привʼязки замало.

    помʼякшують пре-сесіями й токеном у самій формі логіна: перевірка «чи є токен» не закінчується на екранах під автентифікацією.

    І дві межі, за якими токен не рятує. XSS перемагає всі техніки помʼякшення CSRF, і каталог слабкостей ставить свободу від XSS передумовою захисту (дім теми — XSS: міжсайтовий скриптинг). (client-side CSRF), у якому запит формує власний JavaScript застосунку, здатний обійти і токени, і SameSite-кукі: «токен у розмітці є» ще не означає «токен перевірено».

    SameSite: три значення й межа «сайту»

    Атрибут SameSite обмежує область кукі так, що вона додається до запиту лише тоді, коли запит є same-site. Задум названо в чернетці rfc6265bis прямо: значення Lax уведено як механізм «з коробки», що помʼякшує CSRF-атак, роблячи виняток для частини крос-сайтових запитів.

    • Strict — кукі надсилається лише з same-site запитами; браузер не додасть її до жодного крос-сайтового, зокрема й до верхньорівневої навігації з крос-сайтового контексту документа.
    • Lax — same-site плюс крос-сайтові верхньорівневі навігації: у крос-сайтовому запиті кукі проходить тоді й тільки тоді, коли виконано обидві умови: запит є навігацією верхньорівневого контексту перегляду І запит ужив безпечний метод. GET безпечний, POST — ні.
    • None — кукі йде і з same-site, і з крос-сайтовими запитами; без Secure браузер таку кукі просто не прийме.

    так

    ні

    Strict

    None плюс Secure

    Lax

    так

    ні

    Запит до сайту

    Запит same-site

    Кукі йде

    Значення SameSite

    Кукі не йде

    Верхньорівнева навігація безпечним методом

    так

    ні

    Strict

    None плюс Secure

    Lax

    так

    ні

    Запит до сайту

    Запит same-site

    Кукі йде

    Значення SameSite

    Кукі не йде

    Верхньорівнева навігація безпечним методом

    Далі два місця, де тест дає несподіваний результат.

    Кукі без атрибута — не те саме, що явний Lax. Кукі без SameSite трактується як SameSite=Lax, але типова поведінка Chrome дозвільніша: вона пропускає частину кукі на верхньорівневих POST. Чернетка називає режим поіменно — Lax-allowing-unsafe не є значенням атрибута, це режим, який user agent MAY застосувати лише до кукі без явного SameSite; його обмежують віком кукі (розумною межею названо дві хвилини) і прямо оголошують тимчасовим перехідним заходом, що дає менший захист. Звідси й порада виставляти атрибут явно. А друкарська помилка у значенні атрибут не «вимикає»: значення поза трьома відомими ключовими словами підпадає під дефолтний режим, еквівалентний Lax.

    Межа атрибута — сайт, а не походження. https://foo.example.org і https://bar.example.org — різні походження, але той самий сайт: кукі, поставлена на app.example.com із будь-яким значенням SameSite, лишається same-site для запитів із будь-чого в .example.com. «Сайт» тут — (registrable domain): плюс мітка ліворуч від нього, у щоденній мові eTLD+1. Що є суфіксом, визначає список публічних суфіксів, і в ньому не лише домени верхнього рівня — є й сервіси на кшталт github.io, тому your-project.github.io і my-project.github.io рахуються різними сайтами. Схема входить у межу навмисно, щоб HTTP не був слабким каналом. Наслідок важливий: уразливий піддомен знімає цей захист цілком.

    Наскільки міцний це рубіж

    Тут доводиться зробити те, чого в навчальному тексті зазвичай не роблять, — навести чотири канонічні формулювання поруч. Вони не про різні факти, а про різну силу заяви, і зведення їх до одного речення бреше.

    • Чернетка rfc6265bis про Strict: кукі SameSite дають надійний захист від CSRF, коли розгорнуті в суворому режимі й підтримані клієнтом, — і одразу ж радить не робити цю позначку межею всього захисту сайту, бо same-site навігації можна виконати вкупі з іншими векторами (XSS, зловживання редиректами). Про Lax та сама чернетка каже слабше: розумна проти атак на небезпечних методах, але не надійний захист від CSRF як категорії загалом.
    • OWASP: контроль глибини захисту, який у більшості розгортань не замінює повноцінного CSRF-захисту й має поєднуватися з токеном або double submit.
    • MDN: дає певний захист, це не повний захист, краще як доповнення до одного з інших.
    • web.dev: ані Strict, ані Lax не є повним розвʼязанням безпеки сайту.

    Переказу «SameSite не захищає» не робить жодне з цих джерел; беззастережного «SameSite закриває CSRF» — теж жодне.

    Чому «замало» — видно з конкретних щілин:

    • Стано-змінна операція, доступна через GET. Lax блокує лише небезпечні методи, а на верхньорівневих навігаціях безпечними (GET, HEAD, OPTIONS, TRACE) кукі проходить. OWASP називає це найпоширенішим способом, у який захист на базі SameSite провалюється на практиці; чеклист MDN відповідає окремим пунктом — уникати GET для стано-змінних запитів.
    • Верхньорівневу навігацію можна влаштувати. Атакувальник відкриє нове вікно або запустить навігацію, щоб створити «same-site» запит; чернетка називає атрибут тут лише «лежачим поліцейським» на дорозі до експлуатації.
    • Підресурси вже same-site. Крос-сайтовий верхньорівневий запит на чутливу сторінку кукі SameSite=Strict не отримає — а запити за підресурсами цієї сторінки отримають.
    • Перезавантаження тягне все. Запити, видані для перезавантажень, ініційованих не користувачем, додають усі SameSite-кукі.

    Чеклист MDN ставить пріоритет: Strict, якщо можеш, Lax, якщо мусиш. Ціна Strict — перший запит користувача, який прийшов за чужим посиланням, піде без кукі; канонічний вихід тут — дві кукі, де окрема Strict-кукі «на запис» своєю відсутністю запускає повторну автентифікацію перед неідемпотентною дією.

    І межа інструмента, важлива для AQA: читання атрибута з тесту дока Selenium обіцяє в Chrome 80+, Firefox 79+ і Selenium 4 — на старішому стенді «перевірка пройшла» означає «перевірки не було».

    Ендпоінт без форми: кастомний заголовок, CORS і Sec-Fetch-Site

    Багато сучасних вебзастосунків не вживають тегів <form> узагалі. Для AJAX- і API-ендпоінтів OWASP дає окремий захід — запиту (custom request header), і токен для нього не потрібен: клієнт просто додає власний заголовок до запитів, що потребують CSRF-захисту.

    Тримається це не на секретності заголовка, а на механіці браузера: запити з кастомними заголовками всі сучасні браузери позначають такими, що підлягають CORS-preflight, — спершу летить OPTIONS, який перевіряє відповідність CORS на цільовому сервері. З боку атаки MDN каже те саме: такі запити за замовчуванням не дозволені , тож CSRF-атака не вдалася б. Звідси системне формулювання: сайт, що вживає fetch() або XMLHttpRequest, захищається тим, що його стано-змінні запити ніколи не є «простими запитами».

    Тепер важлива частина. Обмеження, на якому все це стоїть, послаблює сам сервер: протокол CORS (дім теми — CORS і політика одного походження) для того й існує. Умова вразливості названа поіменно й читається по заголовках відповіді: сайт буде вразливим до CSRF-атаки з певного походження, якщо відповідь на стано-змінний запит містить Access-Control-Allow-Origin із переліченим у ньому походженням відправника і заголовок Access-Control-Allow-Credentials.

    HTTP/1.1 200 OK
    Access-Control-Allow-Origin: https://partner.example
    Access-Control-Allow-Credentials: true

    Кукі в крос-оріджин запитах за замовчуванням не встановлюються — саме Access-Control-Allow-Credentials: true їх і вмикає. Названий і типовий дефект конфігурації: менш безпечно дозволяти CORS з усіх піддоменів , бо, захопивши піддомен (з хмарними сервісами це не рідкість), атакувальник обійде й підробить запит разом із вашим кастомним заголовком.

    Тому дві речі тримають окремо: CORS — про читання відповіді, CSRF — про саму дію, яка відбувається й тоді, коли відповідь прочитати неможливо. Конфігурація CORS тут — частина поверхні, яку перевіряють; неправильний CORS як самостійний вектор — глава HTTP-заголовки безпеки, CSP і клікджекінг.

    Друга щілина API — content-type: JSON-API природно потребує CORS, але якщо він приймає text/plain, це робить його вразливим, бо простий content-type відтворюється звичайною формою. Захід — заборонити прості content-type на сервері чи в API.

    А найшвидший спосіб побачити, як браузер класифікував запит, — заголовок Sec-Fetch-Site: він каже серверу, чи запит same-origin, same-site, cross-site, чи ініційований безпосередньо користувачем. Значенню можна обґрунтовано довіряти: заголовки з префіксом Sec- не змінити з JavaScript, і браузер ставить їх завжди.

    POST /api/transfer HTTP/1.1
    Host: bank.example
    Origin: https://evil.example
    Sec-Fetch-Site: cross-site
    Content-Type: text/plain;charset=UTF-8

    Як відтворити і що вважати доказом

    Мета перевірки сформульована в WSTG як інваріант: зʼясувати, чи можливо ініціювати запити від імені користувача, які не ініційовані самим користувачем.

    Стано-змінний GET форми не потребує взагалі: коли користувач завантажує сторінку, браузер намагається дістати ресурс-картинку, який насправді є запитом . Каталог слабкостей перелічує носії ширше — URL, завантаження картинки, XMLHttpRequest тощо, — і всі вони будуть трактовані як автентичний запит клієнта.

    Для POST рецепт трикроковий: створити HTML-сторінку з відповідною формою, розмістити її на сторонньому сайті, надіслати посилання жертві й спонукати клікнути. Те, що ендпоінт приймає лише POST, захистом не є: такі запити автоматизуються через JavaScript. Для JSON-ендпоінта обхід описаний окремо: у форматі JSON немає параметрів запиту, обовʼязкових для самонадсильних форм, тому enctype міняють на text/plain, щоб пейлоад доставився як є, а зайве поле-паддинг сервер проігнорує.

    І межа, про яку забувають: ціль не мусить бути в інтернеті — достатньо, щоб адреса була досяжна жертві, а не атакувальнику, тож внутрішня адмінка з цього кола не випадає. Обсяг наслідків обмежений привілеями жертви: атакувальник дістає лише те, що відкрито вразливим застосунком і правами цього користувача.

    Тепер про доказ. Найтиповіша хиба зафіксована в каталозі слабкостей прикладом коду, який «убезпечує» надсилання форми перевіркою, що користувач має валідну сесію. Вердикт джерела: CSRF-атаку цей контрзахід не спинить, бо атакувальник підробляє запит через браузер користувача, у якому валідна сесія вже існує. Перевірка Referer названа разом зі своєю ціною: вона може зламати легітимну функціональність, бо користувачі або могли вимкнути її надсилання з міркувань приватності. І пряма вимога того ж каталогу — не вживати GET для запитів, що змінюють стан.

    Останнє — про межу самої перевірки: гайд тестування заходів запобігання не перелічує, а віддає їх до OWASP CSRF Prevention Cheat Sheet. Тобто перевірка, яка спинилася на відтворенні, ще не сказала, чи достатній захід.

    Типові помилки

    «У нас POST, чужа форма нам не страшна». Виглядає як рубіж, а насправді такі запити автоматизуються через JavaScript, і саме лише вживання методу наявності вразливості не виправляє.

    «Токен у запиті є — захист працює». Виглядає як доказ, а насправді доводить лише те, що значення передали: захистом є звірка з токеном сесії. А перевірка «сесія валідна» контрзаходом проти CSRF не є взагалі — запит іде через браузер, у якому сесія вже жива.

    «У нас JSON-API, формою такого не надішлеш». Виглядає як імунітет, а насправді ендпоінт, що приймає text/plain, відтворюється звичайною формою зі зміненим enctype.

    «Стоїть SameSite=Lax — CSRF закрито». Виглядає як закритий пункт, а насправді Lax блокує лише небезпечні методи: мутація, досяжна через GET, проходить. І кукі без атрибута поводиться інакше, ніж кукі з явним Lax, на верхньорівневих POST — два схожі тести дадуть різний результат, і обидва будуть правильними.

    «У нас налаштований CORS — від CSRF він і захищає». Виглядає як рубіж, а насправді CORS вирішує, чи дозволено прочитати відповідь, тоді як CSRF — про саму дію, яка відбувається й тоді, коли відповідь прочитати неможливо. Гірше: та сама конфігурація буває стороною послаблення — пара Access-Control-Allow-Origin із походженням відправника плюс Access-Control-Allow-Credentials і є названою умовою вразливості.

    «Внутрішня адмінка в інтернет не дивиться — їй CSRF не загрожує». Виглядає як ізоляція, а насправді достатньо, щоб адреса була досяжна жертві: браузер співробітника ходить і всередину периметра.

    Підсумок

    1. Відбір важливіший за експлойт. Три умови діють разом: запит змінює стан, упізнання лише за кукі, параметри передбачувані. Вердикт ухвалюють за передумовами, а не за наявністю робочого експлойта.
    2. Токен ламає передбачуваність — і лише тоді, коли сервер його звіряє з токеном сесії. Не через кукі, не в URL, генерує сервер. Наявність значення в розмітці доказом захисту не є.
    3. SameSite міряє сайт, а не походження. Уразливий піддомен знімає його цілком, а стано-змінний GET знецінює Lax — це названа найчастіша причина провалу на практиці.
    4. Сила заяви про SameSite різна в чотирьох канонічних джерелах — від «надійного захисту в суворому режимі» до «не повного захисту». Ані «не захищає», ані «закриває CSRF» чесним переказом не є.
    5. Для ендпоінтів без форм рубіж — кастомний заголовок, і тримається він на дефолтній забороні браузера. CORS-конфігурація цю заборону послаблює; XSS знімає захід цілком.

    Можливі питання

    «Що таке CSRF і чому браузер узагалі виконує чужий запит?» Дивляться, чи розводите ви дві речі: кукі доводить, що запит іде від браузера користувача, але не доводить, що його ініціювала сторінка застосунку. Сильна відповідь одразу називає умови, за яких атака складається.

    «Чим від цього захищаються і чому саме так?» Очікують механізм, а не список: токен ламає передбачуваність параметрів, і нормативна частина — звірка з токеном сесії. Плюс вимоги: генерує сервер, не через кукі, не в URL і логах.

    «У чому різниця між Strict, Lax і None Питання-фільтр на точність. Lax пропускає крос-сайтовий запит лише на верхньорівневій навігації безпечним методом; None без Secure браузер не прийме; кукі без атрибута і кукі з явним Lax — різні сценарії.

    «Чи достатньо SameSite, щоб закрити CSRF?» Тут перевіряють обережність формулювань. Сильна відповідь називає щілини поіменно — стано-змінний GET, межа по сайту замість походження, підресурси Strict-сторінки — і не зводить різні за силою заяви джерел до одного гасла.

    «Як ви перевірите ендпоінт на CSRF?» Хочуть процедуру: відбір за трьома умовами, відтворення (GET картинкою, POST самонадсильною формою, JSON через text/plain) і окремо — підтвердження захисту, бо «сесія перевіряється» контрзаходом не є.

    «Коли API вразливе?» Слухають, чи не підміните ви критерій форматом. Питання не в JSON проти форми, а в тому, чим сервер упізнає користувача в цьому запиті й чи послаблена дефолтна заборона браузера: пара Access-Control-Allow-Origin із походженням відправника плюс Access-Control-Allow-Credentials — названа умова вразливості, а не деталь конфігурації.

    Джерела

    Три умови, за яких ендпоінт вразливий

    Анти-CSRF токен: що саме він ламає

    • OWASP — CSRF Prevention Cheat Sheet — синхронізований токен і звірка з токеном сесії, вимоги до значення, розподіл заходів за архітектурою, підписаний double submit, login CSRF, XSS і клієнтський CSRF як межі.
    • MDN — Cross-site request forgery (CSRF) — означення токена й умова передбачуваності параметрів, яку він ламає.
    • CWE-352 — Cross-Site Request Forgery (CSRF) — механіка double submit і опора на політику одного походження, XSS як передумова захисту, непередбачуваність значення як окрема слабкість.

    SameSite: три значення й межа «сайту»

    Наскільки міцний це рубіж

    Ендпоінт без форми: кастомний заголовок, CORS і Sec-Fetch-Site

    • OWASP — CSRF Prevention Cheat Sheet — кастомний заголовок як захід для AJAX/API, опора на CORS-preflight, Access-Control-Allow-Credentials і кукі, регулярка по піддоменах, прості content-type у JSON-API.
    • MDN — Cross-site request forgery (CSRF) — заборона крос-оріджин запитів із кастомними заголовками за замовчуванням, правило «ніколи не прості запити», умова вразливості по парі заголовків відповіді, Sec-Fetch-Site як основний захист.
    • web.dev — "Same-site" and "same-origin" — значення Sec-Fetch-Site і чому заголовкам із префіксом Sec- можна довіряти.
    • MDN — Cross-Origin Resource Sharing (CORS) — CORS як механізм доступу до відповіді, а не барʼєр проти самої дії.

    Як відтворити і що вважати доказом

    • OWASP WSTG — 4.6.5 Testing for Cross Site Request Forgery (WSTG-SESS-05) — мета перевірки, трикроковий рецепт для POST, POST як недостатній захід, enctype і паддинг для JSON, досяжність цілі за фаєрволом, відсилання по заходи до окремого документа.
    • CWE-352 — Cross-Site Request Forgery (CSRF) — носії підробленого запиту, «валідна сесія» як хибний контрзахід, ціна перевірки Referer, заборона стано-змінного GET, обсяг за привілеями жертви.
    • MDN — Cross-site request forgery (CSRF) — картинка, яка насправді є запитом транзакції.
    • OWASP — CSRF Prevention Cheat Sheet — межа наслідків: тільки можливості, відкриті застосунком, і права користувача.

    Пояснення

    «Поясни» працює з власним API-ключем Anthropic: запит іде з вашого браузера прямо до Anthropic.

    Свій API-ключ (BYOK)

    Вставте власний ключ Anthropic — пояснення працюватиме на реальній моделі. Ключ зберігається лише у вашому браузері (localStorage), нікуди не надсилається, крім api.anthropic.com, і не логується.