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

    11 · Security для QA

    HTTP-заголовки безпеки, CSP і клікджекінг

    Зміст

    (HTTP security response headers) — дешевий шар захисту: сервер додає кілька рядків у відповідь, і браузер вмикає механізми проти міжсайтового скриптингу (cross-site scripting, XSS), й розкриття інформації. Впровадження коштує мало, перевірка автоматизується тривіально — тому це типовий вміст релізного чекліста.

    Підступність у тому, що перевірка «заголовок присутній» дешева, але тупа. Найдорожчі дефекти виглядають як правильна конфігурація: ALLOW-FROM, який браузер ігнорує цілком; default-src 'none', який не закриває фреймінг; , який нічого не блокує; прибраний , який діє до кінця max-age. Тому нижче не перелік «що поставити», а межі кожного механізму. Механіка самої CSP, CORS, (same-origin policy) і TLS тут не переказується — їхній дім у главах HTTPS, TLS і безпека та CORS і політика одного походження.

    Клікджекінг: клік, який іде не туди

    Клікджекінг (clickjacking) — , у якій змушує користувача взаємодіяти з цільовим сайтом не так, як той намірявся. Друга офіційна назва — UI redress attack: прозорі чи непрозорі накладки підмінюють те, куди насправді йде клік. класифікує його як клієнтську проблему й підмножину UI redressing; термін увели Джеремая Ґроссман і Роберт Гансен у 2008 році.

    Сайт-приманка вбудовує ціль у <iframe>, ховає фрейм стилями й підганяє власні підставні елементи рівно під ті, що виконують чутливі дії. автентифікована жертва: клік іде на невидиму кнопку цільового сайту, і оскільки користувач залогінений, запит несе його справжні облікові дані й проходить. Канонічний приклад OWASP — «click here for a free iPod» поверх кнопки «delete all messages» із фрейму пошти жертви; тим самим прийомом перехоплюють і натискання клавіш.

    Так

    Ні

    Жертва відкриває сайт-приманку

    Приманка вантажить ціль
    у прихований iframe

    Підставна кнопка суміщена
    з чутливою кнопкою цілі

    Клік іде у невидимий фрейм

    Жертва залогінена
    на цілі?

    Запит несе справжню сесію,
    дія виконується

    Дія не проходить

    Так

    Ні

    Жертва відкриває сайт-приманку

    Приманка вантажить ціль
    у прихований iframe

    Підставна кнопка суміщена
    з чутливою кнопкою цілі

    Клік іде у невидимий фрейм

    Жертва залогінена
    на цілі?

    Запит несе справжню сесію,
    дія виконується

    Дія не проходить

    Чому це не CSRF: дії йдуть зі справжньої цільової сторінки, тож частину анти-CSRF захистів атака обходить. Показовий сценарій WSTG — звʼязка CSRF плюс клікджекінг, якою обходять анти- і змушують жертву власноруч підтвердити переказ коштів; сама підробка запиту — у главі CSRF і SameSite.

    Захистів OWASP називає три незалежні. Перший — заборона фреймінгу заголовками (X-Frame-Options або frame-ancestors). Другий — SameSite на сесійних cookie: значення Strict/Lax не потрапляють у запити зі сторінки всередині чужого <iframe>, але якщо атака автентифікації не потребує, атрибут не дає захисту взагалі. Третій — , клієнтський скрипт у самій сторінці, який перевіряє, що поточний фрейм і є верхнім вікном; його призначення — застарілі браузери, а наївний однорядковий варіант захистом не є: OWASP ставить його в розділ «Insecure Non-Working Scripts DO NOT USE».

    Процедура WSTG-CLNT-09 має два кроки: зрозуміти, який захист стоїть, і оцінити, чи він обходиться. Базова техніка — власна сторінка з <iframe> на цільову. Критерій провалу однозначний: сторінка завантажилася у фрейм — захисту немає. Зворотний висновок значно слабший: не завантажилася — захист «якийсь» є, але це ще не імунітет. Три деталі, які ловлять на практиці: клієнтський frame busting відпадає разом із вимкненим JavaScript; мобільна версія часто захищена гірше; веб- відомі тим, що знімають заголовки. Звідси головне правило глави: вердикт знімається з реальної відповіді клієнту, а не з конфігурації сервера.

    X-Frame-Options: старий заборонний заголовок

    X-Frame-Options каже браузеру, чи можна рендерити документ у <frame>, <iframe>, <embed> чи <object>. Дефолт небезпечний, і саме це робить заголовок предметом перевірки: якщо його не надіслано і сайт не має іншого механізму обмеження, браузер дозволить іншим сайтам вбудовувати цей документ.

    ЗначенняЩо означає
    DENYЖоден фрейм, незалежно від походження — включно з вбудовуванням самим сайтом
    SAMEORIGINДозволено, лише якщо всі фрейми-предки того самого походження
    ALLOW-FROM uriЗастаріла директива: сучасні браузери ігнорують заголовок повністю

    DENY OWASP і WSTG називають рекомендованим значенням, поки не доведено потребу у фреймінгу. У SAMEORIGIN є нюанс, який пропускають: перевіряється не самий лише безпосередній батько — чужий фрейм десь вище ламає умову. А ALLOW-FROM — найгірший стан: конфігурація виглядає захищеною, а захисту немає, бо браузер без підтримки відкидає весь заголовок.

    Далі — межі, і кожна породжує або дефект, або хибну знахідку:

    • Метатег не працює: <meta http-equiv="X-Frame-Options"> у розмітці — дефект конфігурації, а не захист. OWASP виносить це в «Common Defense Mistakes» і поширює те саме правило на frame-ancestors.
    • Політика посторінкова, звідси вимога ставити заголовок на всі відповіді з HTML-вмістом.
    • Мультидоменні сайти не обслуговуються: списку доменів задати не можна, браузер шанує рівно один заголовок і одне значення.
    • Межа застосовності: на редиректі або JSON-відповіді API заголовок не дає жодної безпеки, і вимагати його там означає плодити хибні знахідки.

    Статус заголовка джерела формулюють із різною силою: шпаргалки OWASP кажуть, що frame-ancestors obsoletes X-Frame-Options; MDN називає директиву заміною, але радить ставити старий заголовок на додачу, а позначку «obsolete» на сторінці самого заголовка несе лише ALLOW-FROM. Висновок збігається попри різні слова: ставлять обидва — директиву як чинний механізм, заголовок як «graceful degradation and older browser compatibility».

    frame-ancestors: чинний механізм і три пастки

    frame-ancestors — директива CSP, яка перелічує дозволених батьків, що можуть вбудувати сторінку через <frame>, <iframe>, <object> або <embed>. Специфікація формулює її через : директива обмежує URL, які можуть вбудувати ресурс, і дозволяє уникнути багатьох атак класу UI redressing. За класифікацією CSP це navigation-директива — клас, що керує не завантаженням ресурсів, а тим, хто кого вбудовує.

    frame-ancestors 'none' забороняє всім, і одинарні лапки обовʼязкові — це ключове слово, а не рядок; OWASP називає це значення рекомендованим, поки не виявлено потреби у фреймінгу. Кілька дозволених батьків задають списком джерельних виразів через пробіл — головна перевага над старим заголовком (frame-ancestors 'none'X-Frame-Options: deny).

    Три пастки, на яких ловляться навіть автори політик:

    1. frame-ancestors — не frame-src. Перша каже, хто вбудовує нас, друга — звідки вантажаться фрейми в нас. Політика, що обмежує власні фрейми сторінки, від клікджекінгу не захищає ніяк.
    2. Директива не відкочується на default-src. Політика з default-src 'none' усе одно дозволяє вбудовувати ресурс будь-кому, і специфікація фіксує це окремою нотаткою. Найсуворіший на вигляд default-src захистом від клікджекінгу не є — це типовий хибний висновок у ревʼю політик.
    3. У <meta> директива не діє: специфікація робить це нормативним — директиву MUST ігнорувати, якщо політику оголошено метатегом.

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

    Пріоритет нормативний: за наявності frame-ancestors у режимі «enforce» X-Frame-Options MUST ігноруватися; застереження шпаргалки OWASP важливе для звітів — старі версії браузерів (Chrome 40, Firefox 35) цю вимогу порушували. А аргумент «ставимо тільки X-Frame-Options, бо сумісність» датою підтримки не підтверджується: MDN позначає директиву як well established і доступну в браузерах із січня 2018 року.

    Так

    Так

    Ні

    Ні

    DENY / SAMEORIGIN

    ALLOW-FROM

    Немає

    Браузер отримав відповідь

    Є CSP із frame-ancestors
    у режимі enforce?

    X-Frame-Options ігнорується

    Кожен предок
    у списку дозволених?

    Фрейм вантажиться

    Завантаження скасовано

    Є X-Frame-Options?

    Рішення за значенням заголовка

    Заголовок відкинуто повністю

    Вбудовування дозволене будь-кому

    Так

    Так

    Ні

    Ні

    DENY / SAMEORIGIN

    ALLOW-FROM

    Немає

    Браузер отримав відповідь

    Є CSP із frame-ancestors
    у режимі enforce?

    X-Frame-Options ігнорується

    Кожен предок
    у списку дозволених?

    Фрейм вантажиться

    Завантаження скасовано

    Є X-Frame-Options?

    Рішення за значенням заголовка

    Заголовок відкинуто повністю

    Вбудовування дозволене будь-кому

    Із чого складається політика CSP

    Що таке CSP і якими каналами вона доставляється — тема глави HTTPS, TLS і безпека. Тут — склад політики і чому «CSP є» не дорівнює «CSP захищає».

    Політика — впорядкований набір директив, кожна з яких керує однією поведінкою; директива є парою «імʼя / значення», у заголовку директиви розділені крапкою з комою. Специфікація ділить їх на чотири класи: fetch (звідки вантажити ресурси певного типу — script-src, font-src), document (властивості документа: base-uri обмежує URL для <base>), navigation (form-action, frame-ancestors) і reporting (куди слати звіти). Читати політику треба разом із запасними значеннями: default-src є запасною для всіх fetch-директив, script-src — для script-src-attr і script-src-elem, child-src — для frame-src і worker-src. Тому відсутність script-src не означає «дозволено все» — і дзеркально, frame-ancestors на default-src не відкочується.

    Слабку політику видно просто текстом заголовка. За наявності default-src або script-src inline-JavaScript за замовчуванням не виконується, і заборона поширюється й на inline-обробники подій — 'unsafe-inline' її знімає. 'unsafe-eval' дозволяє eval(), але істотно послаблює політику; 'unsafe-hashes' небезпечне за означенням — воно вмикає атаку, у якій вміст inline-обробника інʼєктується в документ як inline-елемент <script>. А host-джерела приймають підстановку * для піддоменів — звідси класика надто широкого allowlist.

    Сучасна альтернатива спискам — nonce і хеші. Nonce — випадкове значення, яке сервер генерує на кожну відповідь; браузер вантажить ресурс, лише коли nonce у директиві збігається з nonce в атрибуті елемента, а впорснутий скрипт не знає, який nonce використає сервер. Три деталі для ревʼю: nonce «гасить» 'unsafe-inline'; мідлвар, що проставляє nonce усім тегам <script>, роздасть його й скриптам атакувальника, тож nonce вставляють шаблонізатором; хеш крихкий — навіть пробіл від автоформатера ламає збіг. 'strict-dynamic' передає довіру від скрипта з nonce до скриптів, які той вантажить динамічно, і ціна подвійна: разом із ним перестають діяти host-джерела, схеми, 'self' і 'unsafe-inline', а якщо довірені скрипти створюють <script> із потенційних джерел XSS, CSP їх уже не прикриє.

    Звідси головний вододіл: початковий підхід — списки дозволених джерел, чинна провідна практика — «строгий» CSP на nonce або хешах, бо його простіше впровадити й важче обійти. Allowlist програє тому, що такі політики ненавмисно впускають небезпечні домени й розростаються до некерованих; попередження джерела пряме — не-строга політика, надто дрібна або надто дозвільна, веде до обходів і втрати захисту. Але щось краще за нічого: навіть default-src https: вимикає небезпечний inline та eval().

    Три речі, які легко пропустити: не успадковує політику документа (її ставлять заголовком на відповідь зі скриптом воркера); кілька політик не послаблюють одна одну; upgrade-insecure-requests не заміняє HSTS, бо не піднімає зовнішні посилання на сайт. Директива require-trusted-types-for 'script' вимагає передавати в injection sinks (eval(), innerHTML, document.write()) лише довірені типи — механізм розгорнуто в главі XSS: міжсайтовий скриптинг.

    І дві межі для чекліста: заголовок ставлять на всі HTTP-відповіді, а не лише на головну сторінку — це типова діра, яку видно саме в тестуванні; але CSP може бути беззмістовним у відповіді REST API, яку ніхто не рендерить, тож «у API немає CSP» — не знахідка.

    Report-режим: як політику обкатують

    Кожна політика має дію (disposition) — або «enforce», або «report». Заголовок Content-Security-Policy-Report-Only віддає політику, яка не примушується: вона нічого не блокує, але порушення не мовчазні — вони пишуться в і летять на , якщо задано report-to чи report-uri. Сенс режиму MDN формулює одним рядком: побачити, які порушення сталися б за цієї політики. Це «fail open» перед переходом у блокувальний «fail closed». Причому обидва заголовки працюють одночасно — звідси робочий патерн: сувора політика в report-режимі збирає порушення, мʼякша в enforce не ламає сайт.

    Для QA це дві протилежні речі. Report-only — готовий інструмент , а не «недовключений захист»: прогін набору тестів проти report-політики показує рівно те, що зламалося б після вмикання. І навпаки: знайдений у проді лише Content-Security-Policy-Report-Only політикою не є.

    • Рекомендований механізм звітності — Reporting API: ендпоінти оголошують заголовком Reporting-Endpoints, ціль для CSP вибирають директивою report-to.
    • Формат: браузер шле звіт як JSON-обʼєкт, методом POST, із Content-Type: application/reports+json — тобто звітність перевіряється мережею.
    • report-uri застаріла: специфікація каже вживати report-to й ігнорувати стару директиву за наявності нової; практична поправка джерел — оголошують обидві, бо старі браузери відкотяться на report-uri.
    • 'report-sample' додає у звіт перші 40 символів заблокованого коду. Без нього видно, що щось заблоковано, і не видно що.

    І межа доставки: заголовок Content-Security-Policy-Report-Only у метаелементі не підтримується — як і директиви report-uri, frame-ancestors і sandbox. Політика в <meta> не звітує й не боронить від фреймінгу.

    HSTS: примус до HTTPS і його незворотні наслідки

    HSTS (HTTP Strict Transport Security) повідомляє браузеру, що до хоста можна звертатися лише через HTTPS і що майбутні HTTP-спроби треба автоматично підвищувати. Це opt-in: хост оголошує себе HSTS-хостом сам, видаючи заголовок Strict-Transport-Security по захищеному транспорту; політика складається рівно з двох речей — «тільки захищений транспорт» і «як довго це памʼятати». Канонічна специфікація — RFC 6797, опублікований IETF наприкінці 2012 року. Другий ефект не менш важливий за перший: на HSTS-хості браузер не дає користувачеві обійти помилку захищеного зʼєднання, зокрема невалідний сертифікат.

    Директив три: max-age — обовʼязкова, скільки секунд від отримання заголовка хост уважається відомим HSTS-хостом; includeSubDomains — необовʼязкова директива без значення, поширює політику на всі піддомени; preload вимагає max-age щонайменше 31536000 (рік) і обовʼязкову присутність includeSubDomains.

    Нормативні межі RFC 6797, з яких ростуть майже всі перевірки:

    • Заголовок по HTTP заборонено: хост MUST NOT вкладати його у відповіді незахищеним транспортом, а браузер такий заголовок ігнорує — знайдений у HTTP-відповіді, він нічого не доводить.
    • Схема переписується до мережевого запиту: браузер MUST при збігу з відомим HSTS-хостом замінити схему на https, а явний порт 80 — на 443; політика перекриває навіть ручний ввід в адресному рядку. Ключем зберігання є доменне імʼя, тож IP-адреса HSTS-хостом бути не може.
    • Будь-яка помилка транспорту рве зʼєднання — «whether "warning" or "fatal" or any other error level»; можливість «клікнути далі» специфікація називає рецептом MITM-атаки. <meta http-equiv> браузери MUST NOT слухати.
    • Зникнення заголовка не скасовує політику: хост лишається відомим до кінця max-age; знімає її лише max-age=0, і лише по HTTPS.

    Піддомени — окрема пастка. includeSubDomains іде вниз по дереву імен, а не вгору: політика для secure.example.com покриває login.secure.example.com, але не example.com; кожен піддомен має віддавати заголовок сам. Специфікація прямо каже, навіщо директива потрібна: без неї HSTS не захищає доменні cookie з прапорцем Secure — ними можна маніпулювати з піддомену.

    Найслабше місце — найперший візит: доки браузер не отримав заголовок по HTTPS, початковий HTTP-запит вразливий (RFC називає це bootstrap MITM vulnerability). Очікувана реакція на HTTP-запит — постійний редирект: хост SHOULD відповісти статусом на кшталт 301 з https-адресою в Location, і в цій редирект-відповіді заголовка HSTS бути не повинно. Проблему першого візиту закриває preload-список: його веде Google й використовують усі браузери, але частиною специфікації він не є, незворотний («PERMANENT CONSEQUENCES»), і сам він домен у список не додає — домен подають окремо. Тому довгий max-age без includeSubDomains OWASP називає небезпечним варіантом, а на час розкочування радить свідомо короткий: ціна помилки асиметрична — неправильний HSTS плюс проблема з сертифікатом, і легітимні користувачі втрачають доступ.

    Для стенду з цього три наслідки: самопідписаний сертифікат на HSTS-хості ламає сценарій остаточно, жорстко зашитий http:// у конфізі до сервера не дійде, а регресію «HSTS зник» ловлять на заголовку, бо стара політика ще діє.

    Поруч у чеклісті транспорту стоїть (mixed content) — його дім теж у главі HTTPS, TLS і безпека, а специфікація HSTS сама зшиває ці теми поняттям «mixed security context». Поведінка браузера коротка: зображення, відео й аудіо автоматично підвищуються з HTTP на HTTPS, решта типів блокується, а (mixed download) типово блокуються з можливістю вибору. Дві поправки до «зображення підвищуються»: це працює для джерела в src, а не в srcset чи <picture>, і підвищення не станеться, якщо хост в URL — IP-адреса, а не доменне імʼя. І пастка стенду: локальні ресурси вважаються захищеними file:-URL і петльові адреси на кшталт http://localhost/, тож локально блокувань ви просто не побачите.

    Referrer-Policy і Permissions-Policy

    Referrer-Policy керує тим, скільки даних про джерело переходу потрапляє в запити. Перше, на чому спотикаються: заголовок запитуReferer, з однією r (історична одруківка), а заголовок політики — Referrer-Policy. Це не лише про приватність: Referer виносить назовні URL, а в URL може лежати ідентифікатор сесії або capability URL — посилання, яке саме по собі дає доступ (скидання пароля, запрошення).

    ЗначенняЩо їде в Referer
    no-referrerНічого, заголовок не надсилається взагалі
    same-originПовний URL усередині свого походження, назовні — нічого
    strict-originЛише походження; нічого при пониженні HTTPS → HTTP
    strict-origin-when-cross-originПовний URL усередині походження, лише походження назовні, нічого при пониженні
    unsafe-urlУсе, включно зі шляхами: «The policy's name doesn't lie; it is unsafe»

    Дефолтну політику треба знати: за MDN це strict-origin-when-cross-origin, і вона ж діє, якщо надане значення невалідне — тобто одруківка не «вимикає» політику, а повертає дефолт. Течуть по незахищеному транспорту origin, origin-when-cross-origin і unsafe-url.

    Чому перевірка «заголовок стоїть» не описує поведінку конкретного запиту: політика надходить пʼятьма шляхами — заголовок, <meta name="referrer">, атрибут referrerpolicy на елементі (<a>, <img>, <iframe>, <script> та інших), rel=noreferrer і успадкування. Механіка пріоритету нормативна: невідомі значення ігноруються, а коли політику задає кілька джерел, перемагає останнє — на цьому тримається трюк із запасною політикою (список через кому, бажана останньою), який працює лише в HTTP-заголовку. Для тесту звідси три правила: OWASP радить не покладатися на дефолт браузера, а слати strict-origin-when-cross-origin на всіх відповідях; unsafe-url — знахідка, а не смак; відсутність Referer у трафіку помилки сервера не доводить, бо браузер MAY дозволяти користувачеві прибрати заголовок. І найцінніший тест тут не про заголовок, а про URL: токен скидання пароля в адресі робить витік реальним.

    Permissions-Policy дозволяє й забороняє можливості браузера в документі та в будь-яких його <iframe> — керує тим, які походження якими можливостями користуються. Це захід зменшення наслідків: навіть за успішної камеру чи мікрофон увімкнути не вдасться. Заголовок задає allowlist для директив — перелік походжень у дужках через пробіл: порожній список () вимикає можливість і зверху, і у вкладених контекстах (типовий hardening — microphone=(), geolocation=()), self лишає її своєму походженню, а маска піддоменів не покриває сам домен.

    Дві поведінки ламають очікування найчастіше. Відсутність директиви не дорівнює забороні: у кожної є власний типовий allowlist — *, self або none. І політика успадковується вниз, але навігація її скидає: щоб можливість працювала у фреймі, його походження має бути й у allowlist батьківської сторінки, а після переходу фрейма на інше походження політика за замовчуванням на нього вже не поширюється. Порушення фіксується в момент спроби скористатися можливістю й збирається через Reporting API, тож перевірка формулюється як сценарій, а не як наявність заголовка. Остання межа: MDN позначає механізм як «not Baseline», бо він не працює в частині найпоширеніших браузерів, — єдиним барʼєром бути не може.

    Ще кілька заголовків, у яких є перевірна вага

    Перелік свідомо не вичерпний — тут те, що регулярно спливає в чеклістах і має підтверджену вагу.

    X-Content-Type-Options: nosniff оголошує, що MIME-типи виставлено свідомо, і забороняє браузеру їх «уточнювати»; так закривається вгадування типу, через яке невиконуваний тип стає виконуваним. Механізмів насправді два: для призначень script і style браузер блокує відповідь при невідповідності типу, а для решти бере заявлений тип як є — і навіть за відсутнього Content-Type відповідь не стане HTML «за замовчуванням». Практичний сценарій: файл, завантажений користувачем, віддано як text/plain, а всередині HTML — із nosniff браузер не витлумачить його як HTML. Єдине значення заголовка — nosniff, і перевіряти його треба в парі з Content-Type. Дзеркально: неправильний Content-Type сам по собі дає XSS — але лише коли вміст призначений для рендерингу клієнтом і він недовірений.

    Два заголовки перевіряються «навпаки». X-XSS-Protection більше не вимагають: він міг захистити користувачів старих браузерів без CSP, але в окремих випадках сам створює XSS-вразливості в інакше безпечних сайтах — тож його не ставлять або явно вимикають (X-XSS-Protection: 0). А Access-Control-Allow-Origin — не захисний заголовок узагалі: без нього сайт типово захищений політикою одного походження, а сам заголовок її послаблює.

    Cross-Origin-Resource-Policy наказує браузеру блокувати і крос-сайт запити режиму no-cors до ресурсу, відсікаючи відповідь ще до потрапляння в процес атакувальника; політику задає власник ресурсу, а значень рівно три: same-site, same-origin, cross-origin.

    Cache-Control потрапляє сюди через класичну пастку: no-cache не забороняє , він лише вимагає ревалідації. Для чутливих даних потрібен no-store, тому no-cache у відповіді з персональними даними — знахідка, а не галочка (механіка директив — у главі Кешування). Server і X-Powered-By — витік версій стека; їх прибирають, але це гігієна, а не захід захисту, а Public-Key-Pins (HPKP) просто мертвий: його прибрали з Chromium у 2018 році. Окремий сценарій — віддача завантажених користувачем файлів: потрібні заголовки, які не дають браузеру виконати вміст, — Content-Disposition: attachment, Content-Type: application/octet-stream і nosniff.

    Неправильний CORS як вектор

    Механіка CORS і політики одного походження — глава CORS і політика одного походження. Тут — той кут, який робить із конфігурації .

    Почати варто з походження як домену захисту: RFC 6454 групує URI в origins — protection domains (схема, хост, порт), і ключова асиметрія там така — надсилати дані в чуже походження дозволено, заборонене саме читання. Звідси й міжсайтова підробка запиту як окремий клас, і CORS як механізм добровільної від заборони на читання.

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

    Дефекти, які шукає тестувальник:

    • * у Access-Control-Allow-Origin — дозволені всі домени; межа прийнятності названа прямо: виправдано лише для публічного API, доступного всім.
    • Віддзеркалення Origin без додаткових перевірок. Підтримка кількох походжень реалізується кодом, який має прочитати Origin, звірити зі списком дозволених і лише за збігу віддати значення назад; пропущений крок звірки і є вразливістю.
    • Access-Control-Allow-Origin: null — окремий дефект: так серіалізується походження sandbox-документів і схем data:/file:, багато браузерів дають таким документам доступ до відповіді, а створити ворожий документ із походженням null може будь-хто.
    • Відсутній Vary: Origin при віддзеркаленні робить помилку масовою: кеш віддасть чужому походженню відповідь, призначену іншому.

    Чому механізм узагалі opt-in, сказано в самій Fetch: інакше течуть дані з-за фаєрвола, а разом з обліковими даними — ще й чутливі; поєднання «ділитися відповідями плюс дозволити облікові дані» специфікація називає доволі небезпечним і вказує клас ризику — confused deputy.

    Процедура WSTG-CLNT-07 формулюється як «знайти ендпоінти з CORS і переконатися, що конфігурація безпечна або нешкідлива»: перехопити заголовки проксі-інструментом, подивитися Origin і дозволені домени — плюс ручний огляд JavaScript. Останнє не для галочки: друга тут не заголовки, а код — URL, що потрапляє в XMLHttpRequest без валідації, і неекранована відповідь, яку підставляють у сторінку.

    Автоматична перевірка наявності заголовків

    Відсутність заголовків — не «недопрацювання», а прояв категорії хибної конфігурації в мапі ризиків OWASP (сама категорія розбирається в главі про security misconfiguration і розкриття даних). Ознака вразливості названа прямо: сервер не надсилає заголовків або директив безпеки, або вони не встановлені в безпечні значення. Пріоритет підпирають два числа: категорія піднялася з пʼятого місця попередньої редакції, і «100% протестованих застосунків» мали ту чи іншу форму хибної конфігурації. Джерело вимагає автоматизованого процесу перевірки конфігурацій в усіх середовищах, а якщо перевірки не автоматизовані — щонайменше ручної щороку.

    Швидкий зовнішній зріз — , інструмент Mozilla, який робить поглиблену оцінку HTTP-заголовків сайту та інших ключових налаштувань безпеки. Результат — не «пройдено/не пройдено», а конкретний перелік того, що виправляти. Механіка бала: старт зі 100, далі штрафи й бонуси у два проходи — перший прохід тільки штрафи, а бонуси нараховуються лише тим, хто після штрафів має 90 (A) або більше; діапазон підсумкового бала — від 0 до 145.

    Найцінніше тут — те, що інструмент каже про себе сам. Межа обʼєктивності проговорена першим абзацом: важко призначити обʼєктивне значення субʼєктивному питанню на кшталт «наскільки погано не впровадити HSTS», і те, що зайве одному сайту, закриває важливі ризики іншому. Оцінки «designed to alert developers» — це сигнал, а не вирок; межі літерних оцінок «essentially arbitrary», а модифікатори змінюються з часом, тож порівнювати бали різних дат наосліп не можна. Звідси й правило: баг заводиться на конкретний заголовок і ризик, а не на літеру.

    Другий канал — читати заголовки просто з відповіді у власному тесті, і тут є пастка. Обʼєкт відповіді Playwright віддає всі заголовки обʼєктом, але обʼєкт не показує повторів: у доці підписано, що в масивній формі імена не приводяться до нижнього регістру, а заголовки з кількома входженнями (як Set-Cookie) зʼявляються в масиві кілька разів.

    const response = await request.get('https://example.com/');
    
    // Обʼєктна форма: одне значення на імʼя — повтори не видно
    const csp = response.headers()['content-security-policy'];
    
    // Масивна форма: усі входження, імена в оригінальному регістрі
    const all = response.headersArray()
      .filter((h) => h.name.toLowerCase() === 'content-security-policy');

    Ще деталь того самого обʼєкта: TLS-дані лежать в окремому полі, для не-HTTPS воно повертає null, а для редиректу віддає дані останнього запиту ланцюга. Куди такі перевірки ставити в й коли знахідка має валити — предмет глави про автоматизацію security-перевірок; тут важливе інше: заголовок читають із самої відповіді, бо проксі на шляху здатен його додати або зрізати, а сканер оцінює те, що віддає публічна адреса.

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

    «Заголовок є в конфізі сервера — захист стоїть». Виглядає як доказ, а насправді доводить лише те, що написано в конфізі: веб-проксі відомі тим, що додають і знімають заголовки, і якщо проксі зріже X-Frame-Options, сайт втратить захист від фреймінгу.

    «default-src 'none' — суворіше не буває, фреймінг закрито». Виглядає як максимальна суворість, а насправді frame-ancestors на default-src не відкочується: таку сторінку може вбудувати будь-хто.

    «Сторінка не вбудувалася в мій <iframe> — усе гаразд». Виглядає як зелений тест, а насправді доводить лише те, що вона не вбудувалася в цьому браузері на цьому URL.

    «Поставили X-Frame-Options: ALLOW-FROM … — дозволили партнеру». Виглядає як тонке налаштування, а насправді сучасний браузер ігнорує весь заголовок, і захисту не лишається зовсім.

    «В API немає CSP і X-Frame-Options — заводимо баг». Виглядає як прогалина, а насправді на JSON-відповіді, яку ніхто не рендерить, ці заголовки нічого не захищають.

    «У відповіді є Content-Security-Policy-Report-Only — CSP присутній». Виглядає як увімкнений захист, а насправді ця політика не примушується за означенням.

    «Прибрали заголовок HSTS — політику знято». Виглядає як відкат, а насправді хост лишається відомим до кінця max-age; знімає політику лише max-age=0 і тільки по HTTPS.

    «У Permissions-Policy немає директиви camera — камеру вимкнено». Виглядає як , а насправді діє власний типовий allowlist директиви, і він може бути *.

    Підсумок

    1. Дефолт майже скрізь небезпечний. Немає X-Frame-Options і frame-ancestors — вбудовувати сторінку дозволено будь-кому; немає директиви в Permissions-Policy — діє її власний дефолт. Перевіряється не «чи немає помилки», а «чи є явне рішення».
    2. Джерело вердикту — сама відповідь, а не її замінники. Метатег для X-Frame-Options, frame-ancestors, report-режиму й HSTS не працює взагалі, а сканер оцінює те, що віддає публічна адреса.
    3. «Механізм є» ≠ «механізм захищає». ALLOW-FROM ігнорується цілком, default-src 'none' не закриває фреймінг, не- веде до обходів, report-only не блокує нічого, а Access-Control-Allow-Origin політику одного походження послаблює, а не підсилює.
    4. Межі механізму — така сама частина перевірки, як його наявність. X-Frame-Options безпредметний на редиректі й JSON, CSP може бути беззмістовним у відповіді REST API, nosniff не рятує без правильного Content-Type.
    5. Стан браузера переживає конфігурацію. HSTS діє до кінця max-age навіть після того, як заголовок зник із відповіді, а preload незворотний — тобто поведінка браузера деякий час нічого не скаже про поточний стан заголовка.

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

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

    «X-Frame-Options чи frame-ancestors Очікують не вибору, а розуміння пріоритету: за наявності директиви в режимі enforce браузер MUST ігнорувати старий заголовок, тож ставлять обидва — директиву як чинний механізм, заголовок як сумісність.

    «Політика default-src 'none' — від чого вона не захищає?» Питання-фільтр на читання CSP. Правильна відповідь одна: від вбудовування сторінки в чужий фрейм. Хто цього не знає, зазвичай і frame-src із frame-ancestors плутає.

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

    «Що робить Content-Security-Policy-Report-Only і коли він шкідливий?» Хочуть дві половини: як інструмент це обкатка політики й готова регресія («що зламалося б»), як стан проду — фальшива галочка. І що обидва заголовки можуть стояти одночасно, і тоді шануються обидві політики.

    «Прибрали HSTS — коли це помітить браузер?» Перевіряють, чи розумієте ви, що політика живе в браузері, а не у відповіді: до кінця max-age, тож регресія ловиться на заголовку. Хороша відповідь додає, що зняти політику можна лише max-age=0 по HTTPS.

    «Чому Access-Control-Allow-Origin: * — це не захист?» Дивляться, чи не переплутали ви напрям дії: без заголовка сайт уже захищений політикою одного походження, а заголовок її послаблює. Тому перевіряють широту, а не наявність, і окремо шукають віддзеркалення Origin без звірки зі списком та значення null.

    Джерела

    Клікджекінг: клік, який іде не туди

    • MDN — Clickjacking — означення атаки, механіка прихованого фрейму, роль автентифікованої жертви, SameSite як помʼякшення.
    • OWASP www-community — Clickjacking — назва UI redress attack, перехоплення натискань клавіш, приклад з iPod.
    • OWASP WSTG — 4.11.9 Testing for Clickjacking (WSTG-CLNT-09) — класифікація й рік появи терміна, звʼязка з CSRF, процедура тесту, вимкнений JS, мобільна версія, проксі.
    • OWASP Cheat Sheet — Clickjacking Defense — три механізми захисту, межа SameSite, призначення frame busting і заборонений однорядковий варіант.

    X-Frame-Options: старий заборонний заголовок

    frame-ancestors: чинний механізм і три пастки

    • MDN — CSP: frame-ancestors — призначення директиви, обовʼязкові лапки, список джерел, відсутність відкату на default-src, недієвість у <meta>, перевірка кожного предка, дата підтримки.
    • W3C — Content Security Policy Level 3 — обмеження вбудовування як захід проти UI redressing, нормативна нотатка про default-src, вимога ігнорувати директиву в метатезі.
    • MDN — Content-Security-Policy (заголовок відповіді) — клас navigation-директив.
    • OWASP Cheat Sheet — Clickjacking Defense'none' як рекомендоване значення, правила лапок, цитата специфікації про пріоритет і застереження про старі браузери.
    • MDN — Clickjacking — браузер із підтримкою директиви ігнорує старий заголовок.
    • MDN — Content Security Policy (CSP) — директива як гнучкіша заміна X-Frame-Options проти клікджекінгу.

    Із чого складається політика CSP

    • W3C — Content Security Policy Level 3 — політика як набір директив, директива як пара «імʼя/значення», чотири класи, base-uri і form-action, відсутність відкату frame-ancestors.
    • MDN — Content-Security-Policy (заголовок відповіді) — синтаксис, ланцюг запасних значень, host-джерела, 'unsafe-inline' і 'unsafe-hashes', nonce і його пріоритет над unsafe-inline, 'strict-dynamic' та його ціна, воркери й накопичення політик.
    • MDN — Content Security Policy (CSP) — чому nonce непередбачуваний, вставка шаблонізатором, ризик 'strict-dynamic' для XSS-джерел, require-trusted-types-for, upgrade-insecure-requests не заміняє HSTS.
    • MDN — Practical implementation guides: CSP'unsafe-eval', вибір nonce проти хешів, строга політика проти location-based, default-src https: як мінімум.
    • OWASP — Content Security Policy Cheat Sheet — заборона inline-обробників, самообман із nonce у мідлварі, крихкість хешів, allowlist проти строгого CSP, попередження про обходи, заголовок на всіх відповідях.
    • OWASP — HTTP Security Response Headers Cheat Sheet — беззмістовність CSP у відповіді REST API.

    Report-режим: як політику обкатують

    HSTS: примус до HTTPS і його незворотні наслідки

    • RFC 6797 — HTTP Strict Transport Security (HSTS) — оголошення HSTS-хоста, дві складові політики, max-age і includeSubDomains, заборона заголовка по HTTP, переписування схеми й порту, розрив зʼєднання при будь-якій помилці, заборона метатега, збереження політики й max-age=0, доменне імʼя як ключ, Secure-cookie, bootstrap MITM, редирект 301, «mixed security context».
    • MDN — Strict-Transport-Security — призначення заголовка, заборона обходу помилки сертифіката, вимоги preload, напрям дії includeSubDomains, окремий заголовок на кожному піддомені, відсутність HSTS у редирект-відповіді, статус preload-списку, IP-адреса як хост.
    • OWASP — HTTP Strict Transport Security Cheat Sheet — opt-in і RFC 6797, ризик cookie з піддомену, незворотність preload і потреба подати домен, короткий max-age на розкочуванні.
    • OWASP — HTTP Security Response Headers Cheat Sheet — асиметрична ціна помилки конфігурації.
    • MDN — Content Security Policy (CSP)upgrade-insecure-requests не піднімає зовнішні посилання, тож HSTS не заміняє.
    • MDN — Mixed content — підвищення медіа й блокування решти, межі підвищення зображень (src проти srcset, IP замість домену), змішане завантаження, локальні адреси як захищені походження.

    Referrer-Policy і Permissions-Policy

    • MDN — Referrer-Policy — призначення заголовка, пастка написання Referer, значення політики, чинний дефолт і поведінка при невалідному значенні, елементи з атрибутом referrerpolicy, запасна політика списком.
    • W3C — Referrer Policy (Candidate Recommendation, 26 January 2017) — ризик витоку session id і capability URL, небезпечні набори значень, пʼять шляхів надходження політики, ігнорування невідомих значень і перемога останнього, право користувача прибрати заголовок.
    • MDN — Permissions-Policy — синтаксис allowlist, () і self, маска піддоменів і апекс, типовий allowlist кожної директиви, успадкування й скидання при навігації, момент порушення й Reporting API, статус «not Baseline».
    • OWASP — HTTP Security Response Headers Cheat Sheet — рекомендація слати strict-origin-when-cross-origin на всіх відповідях; керування можливостями за походженнями як зменшення наслідків інʼєкції.

    Ще кілька заголовків, у яких є перевірна вага

    • OWASP — HTTP Security Response Headers Cheat Sheet — заголовки як дешевий шар захисту, MIME Confusion, X-XSS-Protection і рекомендація його вимкнути, Access-Control-Allow-Origin як послаблення, проти класу Spectre, пастка no-cache, Server/X-Powered-By, мертвий HPKP, трійка для віддачі файлів, неправильний Content-Type як XSS.
    • MDN — X-Content-Type-Options — два механізми nosniff, поведінка при відсутньому Content-Type, сценарій із завантаженим файлом, єдине значення заголовка, вимога перевіряти в парі з Content-Type.
    • MDN — Cross-Origin-Resource-Policy (CORP) — блокування no-cors запитів, політика на боці власника ресурсу, три значення.

    Неправильний CORS як вектор

    • RFC 6454 — The Web Origin Concept — походження як protection domain і трійка схема/хост/порт, дозвіл на надсилання й заборона на читання, CORS як добровільна відмова від заборони.
    • OWASP — HTTP Security Response Headers Cheat Sheet — заголовок як послаблення політики одного походження й вимога перелічувати конкретні походження замість *.
    • OWASP WSTG — 4.11.7 Testing Cross Origin Resource Sharing — обмеження застосовує клієнт, підробний Origin поза браузером, * і віддзеркалення як два дефекти, процедура перевірки, інʼєкція через XMLHttpRequest.
    • MDN — Access-Control-Allow-Origin — реалізація списку дозволених походжень на сервері, дефект null і вимога Vary: Origin.
    • WHATWG Fetch Standard — чому механізм opt-in, витік інтранет-даних і клас ризику confused deputy.
    • MDN — Cross-Origin Resource Sharing (CORS)Access-Control-Allow-Origin як назва одного дозволеного походження або *.

    Автоматична перевірка наявності заголовків

    • MDN — HTTP Observatory — призначення інструмента, глибина оцінки й характер виводу.
    • MDN HTTP Observatory — Tests and scoring — старт зі 100 і два проходи, поріг 90 для бонусів, діапазон 0–145, межа обʼєктивності, призначення оцінки як сигналу, умовність літерних меж і зміна модифікаторів у часі.
    • OWASP — HTTP Security Response Headers Cheat Sheet — Observatory як онлайн-інструмент перевірки стану заголовків.
    • OWASP Top 10:2025 — A02 Security Misconfiguration — відсутні або небезпечно виставлені заголовки як ознака категорії, підйом із пʼятого місця, «100% протестованих застосунків», вимога автоматизованої перевірки конфігурацій.
    • Playwright — APIResponse class — обʼєкт заголовків проти масивної форми з повторами й регістром, окреме поле TLS-даних та його поведінка на не-HTTPS і на редиректі.

    Пояснення

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

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

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