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

    11 · Security для QA

    XSS: міжсайтовий скриптинг

    Зміст

    Типова перевірка на XSS у виконанні джуна виглядає так: вставити <script>alert(1)</script> у поле пошуку й подивитися, чи вискочить вікно. Алерту немає — «чисто», закриваємо. Насправді перевірено один тег в одному контексті одного типу доставки: пейлоад міг не спрацювати, бо значення поїхало в атрибут, а не в тіло HTML; бо виконання видно не тут, а в адмінці; бо тіло відповіді не змінюється й весь сюжет відбувається в DOM.

    Механізм і базове означення XSS живуть у главі HTTPS, TLS і безпека — тут вони не переказуються. Ця глава — канон сайту за тим, чого там немає: типи доставки, й кодування, небезпечні DOM-сінки, і пейлоади. як категорія — глава Інʼєкції: SQL, NoSQL, command, і report-режим — глава HTTP-заголовки безпеки, CSP і клікджекінг.

    Чому XSS розбирають серед інʼєкцій

    Власної категорії в :2025 у XSS немає — він усередині A05 Injection разом із , і профілі двох різні: XSS — «висока частота / низький вплив», понад 30 тисяч ; SQL-інʼєкція — «низька частота / високий вплив», понад 14 тисяч CVE. Формальна підстава сусідства — дерево : CWE-79 є дочірньою слабкістю класу «Injection» (CWE-74), а живучість пояснюється тим, що уникнення XSS вимагає від розробника великої дисципліни. Спосіб виявлення джерело називає прямо: ревʼю коду разом з автоматизованим тестуванням (включно з ) усіх параметрів, заголовків, URL, cookie, JSON, SOAP і XML-вводів — тобто за входами застосунку, а не за видимими полями форми.

    Збережений і відбитий: різниця в доставці, не в наслідках

    (Stored XSS) — два кроки, рознесені в часі: ввід осідає на сервері (база, форум, лог відвідувань, поле коментаря), а потім жертва вичитує ці дані без того, щоб їх зробили безпечними для рендерингу. Синоніми рівноправні: Stored = Persistent = Type 2, а «HTML Injection» у словнику CWE — не окрема слабкість, а синонім збереженого XSS. Тип тяжчий через аудиторію: заражений вміст віддається всім, хто відкриває сторінку, і щоразу, — тож і місце інʼєкції обирають не навмання: оптимальне те, що бачать або багато користувачів, або ті, хто особливо цікавий . Лог відвідувань тут не випадковий: його заповнює атакувальник, а читає адміністратор.

    (Reflected XSS) тримається на двох запереченнях: ввід негайно повертається у відповіді — у повідомленні про помилку, результаті пошуку, будь-якій відповіді з частиною запиту, — і при цьому його не зробили безпечним для рендерингу і не зберегли. Синоніми: Non-Persistent = Type 1; вкладається в один цикл «запит-відповідь». Доставку робить окремий канал — лист або інший сайт: жертва клацає посилання, код їде на вразливий сайт, той відбиває його в браузер, і браузер виконує код, бо той прийшов від «довіреного» сервера. Формально небезпечний вміст подає сама жертва, а каналом доставки буває й CSRF (CSRF і SameSite).

    Наслідок від типу не залежить — різниця лише в тому, як пейлоад приходить на сервер. Тому «це лише reflected» не є підставою знизити пріоритет, а «у нас сайт лише для читання» — підставою вважати його невразливим. Тип ще й не назавжди: забруднення параметрів HTTP (HTTP Parameter Pollution) здатне підняти дефект із відбитого до збереженого, а після збереження пейлоада WAF атаці вже не завадить. — форма персистентного: пейлоад зберігається на сервері й повертається жертві вже з бекенд-застосунку; джерело обмежується констатацією, що підтвердити його в реальному сценарії важко, і процедури не дає.

    осідає у сховищі

    вертається в тій самій відповіді

    лишається в браузері й іде в сінк

    Ввід атакувальника

    Куди він потрапляє

    Збережений XSS

    Відбитий XSS

    DOM-based XSS

    Виконується в кожного, хто відкриє сторінку

    Виконується в того, хто відкрив посилання

    Виконується без зміни тіла відповіді

    осідає у сховищі

    вертається в тій самій відповіді

    лишається в браузері й іде в сінк

    Ввід атакувальника

    Куди він потрапляє

    Збережений XSS

    Відбитий XSS

    DOM-based XSS

    Виконується в кожного, хто відкриє сторінку

    Виконується в того, хто відкрив посилання

    Виконується без зміни тіла відповіді

    Схема показує лише шлях пейлоада — і саме тут ховається найпоширеніша помилка. Типів не три в один ряд: Stored/Reflected — вісь доставки, Server/Client — вісь місця дефекту, разом це матриця 2×2. названо випадок, коли недовірені дані потрапляють у згенеровану сервером відповідь, а джерелом буває і запит, і сховище: існує і відбитий серверний, і збережений серверний, і так само збережений та відбитий DOM-based.

    DOM-based XSS: у відповіді сервера його немає

    (у частині текстів — type-0) — атака, у якій пейлоад виконується внаслідок зміни DOM-оточення, яким користується власний клієнтський скрипт сторінки, так що цей скрипт починає працювати «неочікуваним» чином. Тип молодший за інші: у 2005 році його визначив Amit Klein.

    Прикмета, яка перевертає методику: сама сторінка — тобто HTTP-відповідь — не змінюється, а клієнтський код сторінки виконується інакше, тож «пошукати пейлоад у тілі відповіді» цей тип не ловить у принципі. Місце інʼєкції теж інше: у відбитому й збереженому атаку впроваджують під час серверної обробки, тут — під час виконання, прямо в клієнті; критерій Type 0 у CWE записано так само. Відповідальність від цього не переїжджає на браузер: увесь код походить із сервера, тож робити його безпечним зобовʼязаний власник застосунку. Найпростіший приклад — значення параметра URL, яке скрипт присвоює властивості innerHTML.

    У таксономії це підмножина , де джерело даних лежить у DOM; сам клієнтський XSS означено через сінк — недовірені дані оновлюють DOM небезпечним JS-викликом. Закрити тип важче: велика , брак стандартизації між браузерами, понад 60 різних сінків, а джерелом може бути будь-що під контролем атакувальника — параметр URL, канал postMessage. Серверні фільтри проти нього не працюють за побудовою, WAF проти DOM-based прямо не рекомендований, а мінімальний приклад зі шпаргалки обходів — недовірений GET-параметр напряму в eval.

    Джерело: параметр URL або postMessage

    Скрипт самої сторінки

    Сінк: innerHTML або eval

    Рядок виконується як код

    Тіло HTTP-відповіді

    Не змінюється, грепом не знайти

    Джерело: параметр URL або postMessage

    Скрипт самої сторінки

    Сінк: innerHTML або eval

    Рядок виконується як код

    Тіло HTTP-відповіді

    Не змінюється, грепом не знайти

    Контексти виводу: правильного екранування «взагалі» не буває

    Мета (output encoding) — перетворити недовірений ввід у форму, де він показується як дані й не виконується як код: небезпечні символи екрануються, щоб їх трактували як текст. Саме це, а не , названо найефективнішим рішенням проти XSS: валідація лише додає шар і зробити її правильно значно складніше, а найпростішим і найсильнішим у більшості випадків захистом від серверного XSS названо поіменно контекстно-залежне кодування виводу на сервері. Захищати треба всі змінні застосунку, а не «підозрілі» поля.

    «Ми все екрануємо» нічого не означає, бо методів багато: браузер по-різному розбирає HTML, JS, URL і CSS, і неправильний метод або вносить слабкість, або ламає функціональність. Різні частини одного документа вимагають різного кодування, а HTML entity encoding доречне лише для тіла HTML.

    Контекст вставкиЩо робити
    Тіло HTML, між тегамиHTML entity encoding
    Значення HTML-атрибутаHTML attribute encoding; безпечні лише атрибути з окремого переліку
    Вбудований JavaScriptєдине «безпечне» місце — рядкове значення в лапках, символи у форматі \xHH
    CSSзначення підставляють лише у значення CSS-властивості
    URLусі символи у форматі %HH, атрибути обовʼязково в лапках

    Три уточнення до таблиці. Лапки — частина захисту: вони ускладнюють зміну контексту й звужують набір символів, які треба кодувати. Атрибути-обробники подій виключені: ті, що приймають JavaScript (onClick), небезпечні з недовіреним значенням у принципі, і MDN додає сюди ж onblur та атрибут src елемента iframe. А вкладені контексти кодують по черзі: URL усередині атрибута спершу URL-кодують, потім застосовують HTML attribute encoding.

    Є й позиції, де кодування не рятує взагалі, — джерело зве їх й вимагає не ставити туди змінних: у контексті виконання закодований рядок виконається попри кодування, а вставка всередину <script> чи <style> майже завжди небезпечна. Коли користувач має право писати HTML, кодування зламає функціональність — тоді застосовують санітизацію: вирізати небезпечний HTML і повернути безпечний, прибравши теги script і вбудовані обробники подій. Санітайзер — жива залежність: обходи знаходять регулярно, тож патчити його треба регулярно.

    Кодуванням виводу не є глобальний фільтр на вході (він не знає контексту, а HTML- і JS-кодування не взаємозамінні), сліпе HTML-кодування «на все» (при типі вмісту text/xhtml може не спрацювати) і JSON.stringify(); ще й саме кодування злітає, коли значення читають через атрибут value DOM-елемента. Найдешевша точка входу для тестувальника — місця, де автоекранування шаблонізатора вимкнене штатно: фільтр safe у Django, dangerouslySetInnerHTML у React.

    Сінки: куди саме приїжджає значення

    (injection sink) — потужна функція вебплатформи, яку можна викликати лише з довіреним, перевіреним або належно санітизованим значенням; виклик із контрольованим атакувальником вводом уже вважається , а — той, у якому недовірене значення дає DOM XSS. Чому такий сінк не видно з коду: рядки не несуть інформації про , тож застосунки регулярно передають туди контрольовані атакувальником значення, не усвідомлюючи цього. Ревʼю «подивитися на виклик innerHTML» відповіді не дає — тому платформний захист і будується на типах.

    Сінків понад 60, і вся група має машинну назву 'script'. До неї належать функції, що парсять і вставляють HTML: сетери Element.innerHTML, ShadowRoot.innerHTML, Element.outerHTML і Document.write. Поіменно небезпечні також:

    • setAttribute(name, value) — неявно приводить рядок до типу DOM-атрибута, тож безпечний лише для обмеженого переліку атрибутів; небезпечні всі, що є контекстом виконання команд, як-от onclick чи onblur.
    • Методи, що приймають код рядкомsetTimeout, setInterval, new Function.
    • Неявний eval — методів, які приховано обчислюють передані дані, багато. Для розбору JSON беруть JSON.parse(): він відкидає все, що не є валідним JSON.

    визначено критерієм «трактує значення як текст і ніколи його не виконує». Базовий безпечний спосіб покласти недовірені дані в DOM — textContent; рефакторинг зводиться до заміни небезпечних сінків на textContent або value, а найсильнішим захистом від клієнтського XSS джерело називає використання безпечних JavaScript API. Фікс тут — змінити сінк, а не докрутити екранування.

    // небезпечно: значення з URL іде в сінк, який парсить HTML
    const name = new URLSearchParams(location.search).get('name') ?? '';
    welcome.innerHTML = `Вітаємо, ${name}`;
    
    // безпечно: сінк трактує значення як текст і не виконує його
    welcome.textContent = `Вітаємо, ${name}`;

    Окремо про innerText, який часто радять як заміну: шпаргалка із запобігання DOM-based XSS застерігає, що залежно від тега код усе-таки може виконатися, а шпаргалка із запобігання XSS у переліку безпечних сінків його не називає взагалі. Для eval заміни немає: недовірений ввід у ньому — завжди погана ідея, і замість спроб санітизувати його просто не роблять.

    Для перевірки це означає, що інвентаризація сінків — скінченна робота: перелік відомий наперед, а сам виклик дефектом не є, бо питання в походженні значення.

    Trusted Types: типи замість рядків

    Trusted Types — API, який замикає потужні веб-API на приймання лише непідробних типізованих значень замість рядків. Він цілить рівно в ту сліпу пляму, через яку живе DOM XSS: створити значення можна тільки через визначені застосунком незмінні політики, тож поверхня зводиться до малих ізольованих ділянок коду. Мета — мінімізувати ймовірність DOM XSS; заявлена не-мета — інʼєкції в розмітку, згенеровану сервером: для них рекомендують шаблонізатори і script-src. Слово «trusted» означає лише впевненість автора, що значення можна безпечно передати в сінк: обгортка нічого не санітизує, зате підробити її не можна.

    Примус вмикається через CSP: require-trusted-types-for задає групи сінків, для яких типи обовʼязкові, trusted-types керує створенням політик. Робочий заголовок — Content-Security-Policy: require-trusted-types-for 'script': у браузерах на Chromium він змушує DOM XSS-сінки відхиляти звичайні рядки. Розкочування має штатну репетицію: спершу Content-Security-Policy-Report-Only, збір порушень — і лише потім примус.

    Головна пастка міграції — політика з іменем default: браузер викликає її неявно щоразу, коли в сінк передали звичайний рядок, і якщо її немає або вона повертає null чи undefined, це дає порушення CSP — у режимі примусу кидається помилка, у report-only початкове значення просто йде далі. І навпаки: лакса no-op зводить нанівець усю користь механізму — ризик не зникає, а переїжджає в саму політику. Плюс доброї крос-браузерної підтримки API ще не має.

    Звідси три наслідки для перевірки: наявність заголовка не означає «увімкнено», бо default у списку рядки не відхиляє; зелений результат у report-only нічого не доводить; а звіт про порушення несе обрізаний пейлоад — 40 символів разом з іменем сінка.

    CSP: другий рубіж, і саме другий

    CSP свідомо не задумана як перша лінія захисту від інʼєкції вмісту — специфікація описує її як . Те саме з боку XSS формулює OWASP: це список дозволеного, помилитися в реалізації якого легко, тож основним механізмом захисту він бути не має; первинними названо захисти фреймворку, кодування виводу й . Якщо політику будують саме проти XSS, рекомендований підхід — на nonce або хешах, які кажуть браузеру, які скрипти він має побачити в документі; дзеркально 'unsafe-inline' і схему data: як дозволені джерела вказувати не слід.

    Межі сусідніх шарів варто тримати в голові: атрибути cookie обмежують шкоду, але не спиняють виконання шкідливого вмісту й не усувають причини, а WAF ненадійний і причини теж не усуває — єдиного прийому не існує, потрібна комбінація. Склад директив і report-режим — глава HTTP-заголовки безпеки, CSP і клікджекінг.

    Пейлоади: що вони доводять і чого не доводять

    Канонічний набір — це посібник із тестування, а не інструкція «як написати фільтр»: шпаргалка демонструє, що фільтрація вводу є неповним захистом від XSS, і саме для цього дає серію атак, здатних обійти певні захисні фільтри. Знайдений обхід — не задача «полагодити фільтр»: лікують кодування виводу й санітизація. Набір починається з baseline — звичайної JS-інʼєкції, — а далі йде один зонд на кілька контекстів, , що виконується одразу в HTML, рядках скрипта, JavaScript і URL:

    javascript:/*--></title></style></textarea></script></xmp> <svg/onload='+/"`/+/onmouseover=1/+/[*/[]/+alert(42);//'>
    

    Пейлоад не зводиться до тега script: зонд може триматися лише на обробнику події, окремий вектор дає поблажливий всередині тега IMG, спрацює й <body onload=alert('test1')> та атрибути на кшталт onmouseover і onerror. Шкідливий вміст часто має форму JavaScript, але може бути HTML, Flash чи будь-яким іншим кодом, який браузер уміє виконати. Класика в розборі MDN: створюється елемент img зі значенням src, яке дає помилку, і саме тому виконується обробник onerror.

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

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

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

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

    «Це лише reflected». Виглядає як тріаж, а насправді наслідок однаковий незалежно від типу — тип описує спосіб доставки, а не шкоду. Те саме з «у нас сайт лише для читання»: доставку робить посилання, а не форма.

    «У відповіді пейлоада немає — значить чисто». Виглядає як доказ, а насправді при DOM-based тіло HTTP-відповіді не змінюється взагалі, і пошук по відповіді цей тип не ловить у принципі.

    «Ми екранували HTML скрізь». Виглядає як суцільний захист, а насправді HTML entity encoding доречне лише для тіла HTML, різні частини документа вимагають різного кодування, а всередині script і style вставка небезпечна навіть закодована.

    «Trusted Types увімкнено, заголовок є». Виглядає як , а насправді default-політика в списку пропускає рядки крізь себе, а в report-only початкове значення взагалі йде далі.

    Підсумок

    1. Тип XSS описує доставку, а не наслідок. Збережений, відбитий і DOM-based відрізняються тим, як пейлоад дістається жертви; шкода однакова, і «лише reflected» аргументом у тріажі не є.
    2. Типів не три в один ряд. Stored/Reflected — вісь доставки, Server/Client — вісь місця дефекту; разом це матриця 2×2, а DOM-based — підмножина клієнтського XSS.
    3. Головний захист — контекстно-залежне кодування виводу, і це не одна дія. Тіло HTML, атрибут, вбудований JS, CSS і URL кодуються по-різному, а є ще й контексти, де кодування не рятує взагалі.
    4. Клієнтську частину лікує сінк, а не фільтр. Виклик сам собою не дефект — питання в походженні значення; правильний фікс — замінити небезпечний сінк на textContent, і саме на цю сліпу пляму цілять Trusted Types.
    5. CSP і WAF — другий рубіж. Політика ускладнює експлуатацію, але не усуває причини; а пейлоади існують, щоб показати неповноту фільтрації, а не щоб поповнювати .

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

    «Які бувають типи XSS і чим вони відрізняються?» Слабка відповідь перелічує три слова; сильна називає критерій кожного — осідання даних на сервері, повернення в тій самій відповіді, зміна DOM без зміни відповіді — і додає, що наслідок від типу не залежить.

    «Чому DOM-based шукають інакше?» Перевіряють розуміння механізму, а не назви. Ключове: HTTP-відповідь не змінюється, тому пошук пейлоада у відповіді безсилий, а джерело значення шукають у самому DOM — параметр URL, postMessage.

    «Як правильно захиститися від XSS?» Дивляться, чи не почнете ви з фільтрації вводу. Очікують кодування виводу з урахуванням контексту, санітизацію там, де користувач має право писати HTML, захисти фреймворку — і лише потім CSP як другий рубіж.

    «Чому innerHTML небезпечний, а textContent ні?» Питання про сінки. Сильна відповідь дає критерій безпечного сінка — трактує значення як текст і ніколи не виконує — і додає, що рядок не несе інформації про походження, тож самого виклику для вердикту мало.

    «Чи достатньо CSP, щоб закрити XSS?» Питання-пастка. Відповідь: ні — специфікація прямо називає CSP глибиною захисту, а не першою лінією; а Trusted Types, які вмикаються тим самим заголовком, закривають лише DOM-сінки й серверних типів не стосуються.

    Джерела

    Чому XSS розбирають серед інʼєкцій

    Збережений і відбитий: різниця в доставці, не в наслідках

    • OWASP — Types of Cross-Site Scripting — означення збереженого й відбитого типів, матриця 2×2, серверний XSS і накладання осей.
    • CWE-79 — Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') — критерії Type 1 і Type 2, «HTML Injection» як синонім збереженого, вибір місця інʼєкції, однаковість наслідку.
    • OWASP — Cross Site Scripting (XSS) — Persistent і Non-Persistent, один цикл «запит-відповідь», доставка листом чи з іншого сайту, застереження про сайт лише для читання, blind XSS і його важкість підтвердження.
    • MDN — Cross-site scripting (XSS) — чому збережений особливо тяжкий: вміст віддається всім користувачам і щоразу.
    • OWASP Cheat Sheet — XSS Filter Evasion — підняття дефекту з відбитого до збереженого через забруднення параметрів і безсилля WAF після збереження пейлоада.

    DOM-based XSS: у відповіді сервера його немає

    Контексти виводу: правильного екранування «взагалі» не буває

    • OWASP Cheat Sheet — Cross Site Scripting Prevention — мета кодування виводу, правила по контекстах, роль лапок, небезпечні контексти, санітизація й вимога патчити санітайзер, глобальний фільтр як хибний захист.
    • OWASP Cheat Sheet — DOM based XSS Prevention — безсилля JS-кодування в контексті виконання, межа сліпого HTML-кодування, JSON.stringify() і втрата кодування при читанні з DOM.
    • CWE-79 — Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') — кодування виводу як найефективніше рішення, різні кодування в межах одного документа, HTML entity encoding лише для тіла.
    • MDN — Cross-site scripting (XSS) — механізм екранування, небезпечні атрибути, санітизація і штатне вимкнення автоекранування в шаблонізаторах.
    • OWASP — Types of Cross-Site Scripting — контекстно-залежне кодування виводу на сервері як найсильніший захист і складність правильної валідації вводу.

    Сінки: куди саме приїжджає значення

    • W3C — Trusted Types — означення сінка інʼєкції та DOM XSS-сінка, відсутність інформації про походження в рядках, понад 60 сінків, група 'script', функції парсингу HTML.
    • OWASP Cheat Sheet — DOM based XSS PreventionsetAttribute і небезпечні атрибути, методи з кодом-рядком, неявний eval проти JSON.parse(), textContent як базовий безпечний спосіб і застереження про innerText.
    • OWASP Cheat Sheet — Cross Site Scripting Prevention — критерій безпечного сінка й перелік textContent / value.
    • OWASP — Types of Cross-Site Scripting — безпечні JavaScript API як найпростіший і найсильніший захист від клієнтського XSS.
    • MDN — Cross-site scripting (XSS) — присвоєння значення властивості innerHTML як типовий небезпечний сінк.

    Trusted Types: типи замість рядків

    • W3C — Trusted Types — суть API, політики й default-політика, примус через require-trusted-types-for і trusted-types, режими enforcing і report-only, обрізаний звіт, модель і межі.
    • OWASP Cheat Sheet — Cross Site Scripting Prevention — заголовок вмикання в Chromium і відхилення звичайних рядків DOM XSS-сінками.
    • MDN — Cross-site scripting (XSS) — стан крос-браузерної підтримки Trusted Types.

    CSP: другий рубіж, і саме другий

    Пейлоади: що вони доводять і чого не доводять

    Пояснення

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

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

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