Cross-site scripting виникає, коли значення, яке application планувала лише показати, стає executable content у browser. Воно може прийти з форми, URL, database, зовнішнього API або compromised dependency. Output encoding є одним із найсильніших захистів, але працює лише для правильного destination context. HTML text, attributes, JavaScript, CSS та URLs є різними мовами з різними правилами.

Один документ містить кілька parser contexts

Browser одночасно розуміє markup, inline scripts, style rules і navigation targets. Значення, безпечне між tags, може завершити JavaScript string. Значення, що не виходить за межі quoted attribute, усе одно може створити небезпечний javascript: URL. Security залежить не від походження string, а від того, як саме browser його parse-ить.

Framework auto-escaping захищає типові text та attribute insertions. Ця гарантія зникає біля raw HTML, string concatenation, unsafe DOM APIs та inline code, тому такі місця повинні бути рідкісними й видимими під час review.

Escape потрібно робити на фінальній межі

Зберігання HTML-encoded input у database змішує semantic data з presentation і часто породжує double encoding. Те саме значення пізніше може піти в JSON, email або CSV, де HTML rules не мають сенсу. Надійніша модель зберігає original value, перевіряє business constraints і encode-ить безпосередньо під час rendering.

Саме output layer знає context. Templates мають escape by default, а кожна операція raw rendering повинна вимагати свідомого рішення та пояснення джерела довіри.

Небезпечних контекстів краще уникати

Untrusted data складно надійно вставляти всередину script blocks, style blocks, event-handler attributes або готових markup fragments. Замість цього дані можна передавати через JSON response чи безпечні data attributes і читати structured API. Для plain text у DOM потрібно використовувати textContent, а не innerHTML.

Такий дизайн взагалі не дає значенню стати кодом. Encoding перетворюється на передбачувану boundary operation, а не спробу нейтралізувати кожен можливий перехід між мовами.

Дозволений rich HTML потребує sanitizer

Коментарі або редактор статей іноді мають підтримувати formatting. Escaping усього markup знищить потрібну структуру, тому trusted sanitizer повинен parse-ити HTML і застосовувати allowlist. Він має враховувати dangerous attributes, URL schemes, malformed markup та реальну browser behavior.

Regex і string replacements не підходять для побудови sanitizer. HTML є structured і error-tolerant language; атакувальник шукатиме саме різницю між спрощеним filter та browser parser.

URL потребує validation крім attribute encoding

Escaping quotes утримає value всередині href, але не зробить destination безпечним. Application повинна parse URL і дозволити очікувані schemes, hosts або internal paths. Open redirects і dangerous schemes залишаються проблемою навіть у синтаксично правильному HTML.

Спочатку перевіряється значення navigation target, а потім на output застосовується attribute encoding. Syntax safety та business navigation policy є двома окремими перевірками.

CSP обмежує наслідки, але не виправляє код

Content Security Policy може заборонити неочікувані scripts, дозволяючи approved code через nonce або hash. Reporting допомагає побачити спроби порушення policy. Проте legacy exceptions, помилки конфігурації та різна browser support означають, що CSP залишається defense in depth.

Основний захист усе ще складається з contextual encoding, safe DOM operations і sanitization там, де markup справді дозволений.

Client-side code також створює XSS

Після безпечного server rendering JavaScript може прочитати location, API response або DOM value і передати його в innerHTML. Тому audit має охоплювати не лише templates, а й DOM sinks. Structured element creation та textContent зберігають data окремо від markup.

Trusted Types у великих applications можуть обмежити небезпечні sinks значеннями, створеними approved policies. Це не замінює sanitizer, але робить приховані assumptions контрольованими.

Тестувати потрібно кінцевий DOM

Security tests повинні містити quotes, angle brackets, ampersands, unusual URL schemes, malformed tags і Unicode edge cases. Важливо перевіряти не лише response source, а й DOM та поведінку browser після роботи client scripts.

Суть contextual encoding не в запам’ятовуванні однієї escape function. Команда повинна знати destination context, використовувати API для цього context і не вставляти untrusted values туди, де вони можуть стати executable content.

Stored XSS може чекати місяцями

Небезпечний payload не обов’язково виконується одразу після введення. Він може зберігатися в profile, support ticket або imported record, а пізніше потрапити в адміністративний dashboard із ширшими правами. Validation лише на публічній формі не захищає всі майбутні rendering contexts.

Кожен output boundary повинен вважати stored data недовіреним незалежно від віку та джерела. Особливої уваги потребують internal tools: їх часто використовують привілейовані operators, але вони мають слабший UI security review.

Framework migration може змінити контекст

Після заміни template engine або client framework старі helpers іноді отримують інші escaping defaults. Компонент, що раніше приймав plain text, може почати очікувати HTML, або навпаки. Такі зміни створюють і XSS, і видиме double encoding.

Migration plan повинен інвентаризувати raw-output operations, custom sanitizers та direct DOM sinks. Regression tests із реальним browser допомагають довести, що межа між data і code збереглася після технічного оновлення.

Incident response потребує видимості

CSP reports, sanitizer rejection metrics і audit raw rendering допомагають знайти проблему до масового exploitation. Logs не повинні повторно render-ити payload у небезпечному dashboard; підозріле значення потрібно показувати як escaped text.

Після XSS incident недостатньо видалити один record. Команда має знайти vulnerable sink, перевірити подібні contexts, invalidate скомпрометовані sessions і визначити, чи payload уже виконувався для інших користувачів.