Информационная безопасность

XSS: Cross-Site Scripting

20 строк кода, 20 часов, 1 миллион заражённых аккаунтов. 2005 год, MySpace. Samy Kamkar не взломал сервер, не украл пароли, не подобрал ключи. Он просто написал JavaScript, который копировал себя в профили всех, кто открыл его страницу. Браузер сделал всё сам - всё по спецификации HTML. XSS-червь Samy остаётся крупнейшим быстрым заражением в истории веба.

  • **British Airways 2018** - Stored XSS на странице оплаты перехватил данные 500 000 платёжных карт за 15 дней. Штраф GDPR - 20 млн фунтов стерлингов.
  • **eBay 2014** - Stored XSS в листингах товаров. Атакующие перенаправляли покупателей на фишинговые страницы прямо с официального eBay.com.
  • **Twitter 2010** - XSS-червь onMouseOver: наведение мыши на твит запускало ретвит без клика. За несколько часов десятки тысяч аккаунтов распространили payload.
  • **Reflected XSS как фишинговый вектор** - корпоративные порталы, банковские личные кабинеты: URL выглядит доверенно, TLS-сертификат настоящий, а в query string спрятан скрипт кражи сессии.
  • **Bug Bounty 2024** - Google, Apple, Meta платят до USD 20 000 за XSS в критических сервисах. Уязвимость не устарела - она адаптируется к SPA, Web Components, Shadow DOM.

Reflected XSS

2005 год. Восемнадцатилетний Samy Kamkar написал 20 строк JavaScript. Разместил на своей странице MySpace. Через 20 часов социальная сеть легла - 1 миллион аккаунтов заражён, каждый из них добавил Samy в друзья и распространил код дальше. Первый XSS-червь в истории. Samy получил 3 года условного срока и запрет на использование компьютеров. Запрет на **использование компьютеров**. В 2005 году.

XSS - это не баг браузера. Это архитектурное недоразумение: сервер доверяет пользовательскому вводу настолько, что вставляет его в HTML буквально. Браузер видит HTML - исполняет. Всё по спецификации.

Механика Reflected XSS

Reflected (отражённый) XSS работает через URL. Жертва кликает по ссылке, содержащей вредоносный payload - сервер «отражает» его обратно в ответе, браузер исполняет. Атака не хранится на сервере - она живёт только в один конкретный момент запроса.

Атакующий не взламывает сервер. Он отправляет **жертве** ссылку, которую та сама открывает. Сессионный cookie, CSRF-токен, LocalStorage - всё что доступно JavaScript, теперь доступно атакующему. Один клик - и аккаунт угнан.

Фишинг с reflected XSS выглядит убедительно: домен настоящий, сертификат настоящий, только в URL спрятан payload. Большинство антивирусов и почтовых фильтров 2010-х годов это не ловили - URL был слишком длинным для визуального анализа, а сокращатели ссылок убирали следы.

В чём принципиальное отличие Reflected XSS от Stored XSS?

Stored XSS

Вот сценарий, который разыгрался на British Airways в 2018 году. Атакующие нашли stored XSS и внедрили 22 строки кода в страницу оплаты. Код работал тихо - перехватывал данные карт и отправлял на сервер в Румынии. За 15 дней утекло 500 000 платёжных карт. Штраф GDPR - 20 миллионов фунтов стерлингов. Один stored XSS в форме ввода - двадцать миллионов фунтов.

Механика Stored XSS

Stored (постоянный, persistent) XSS - payload сохраняется в базе данных сервера. Комментарий на форуме, имя пользователя в профиле, описание товара, сообщение в чате. Любое поле, которое отображается другим пользователям без экранирования.

Stored XSS масштабируется. Один инжект - тысячи жертв. Атака работает без какого-либо взаимодействия с жертвой кроме факта открытия страницы. Популярная страница форума, главная страница сайта, профиль известного пользователя - выбор места размещения определяет охват.

Цель Stored XSS - не только кража cookie. Session hijacking уже не работает, если cookie имеет флаг HttpOnly. Тогда атакующий делает действия от имени жертвы прямо из XSS-кода: меняет email, переводит деньги, добавляет backdoor-администратора. Браузер жертвы становится прокси.

Классический способ борьбы - Content Security Policy. Но до CSP нужно понять третий вектор: тот, где сервер вообще не при чём.

Флаг HttpOnly на cookie защищает от кражи cookie через XSS. Делает ли это Stored XSS безопасным?

DOM-based XSS и CSP

DOM-based XSS - это атака, где сервер **вообще невиновен**. Сервер отдаёт идеально чистый HTML. Уязвимость живёт целиком в клиентском JavaScript, который читает данные из опасных источников (URL, localStorage, postMessage) и вставляет их в DOM без проверки.

Sources и Sinks

В DOM-based XSS критична пара: source (откуда данные) и sink (куда они вставляются). Если между ними нет санитизации - атака возможна.

DOM-based XSS не виден в server-side логах - запрос на сервер выглядит чисто. WAF (Web Application Firewall) на основе правил его не поймает: сервер честно отдал страницу, атака происходит на клиенте. Единственные инструменты - статический анализ JS-кода и CSP.

Content Security Policy

Content Security Policy (CSP) - HTTP-заголовок, который говорит браузеру: «доверяй только тому, что я явно разрешил». Браузер становится союзником: скрипт с неизвестного домена? Заблокирован. inline-скрипт без nonce? Заблокирован. eval()? Заблокирован.

Nonce - случайное значение, генерируемое сервером на каждый запрос. Атакующий не знает nonce заранее - его payload `<script>alert(1)</script>` не имеет nonce и браузер его блокирует. Строгая CSP с nonce - самая надёжная защита от XSS на уровне браузера.

Санитизация vs Экранирование

**Экранирование** (escaping) - замена спецсимволов на HTML-entities при выводе. `<` становится `&lt;`, `>` становится `&gt;`, `"` становится `&quot;`. Браузер отображает символы, но не интерпретирует как HTML. Это must-have для любого вывода пользовательских данных в HTML.

Частая ошибка: санитизировать на сервере, а отображать через innerHTML на клиенте без повторной проверки. Или наоборот - экранировать при сохранении в БД (double-encoding). Правило: **хранить raw данные, экранировать при выводе** в зависимости от контекста: HTML, JS, CSS, URL - каждый контекст требует своего экранирования.

Google, Facebook, Twitter, GitHub - все они платили баунти за XSS-уязвимости в 2020-х. XSS не «старая» проблема. Это проблема любого приложения, которое выводит пользовательский контент. Single-page apps добавили DOM-based вектор. NoSQL-хранилища добавили неожиданные источники данных. Атака адаптируется.

Достаточно фреймворка (React, Angular) - они сами защищают от XSS

Фреймворки защищают от одного вектора - прямой вставки в шаблон. dangerouslySetInnerHTML, bypassSecurityTrustHtml, innerHTML, eval, postMessage без проверки origin - все эти паттерны создают XSS даже в современных фреймворках.

React экранирует JSX-выражения автоматически. Но как только разработчик использует escape-hatch (dangerouslySetInnerHTML) для отображения rich text - защита отключается. DOM-based XSS вообще не зависит от шаблонизатора. CSP и экранирование нужны всегда.

CSP-заголовок содержит `script-src 'unsafe-inline' 'unsafe-eval' *`. Насколько он защищает от XSS?

Ключевые идеи

  • **Три типа XSS** - Reflected (payload в URL, требует клика), Stored (payload в БД, автоматически для всех), DOM-based (клиентский JS читает из source и пишет в sink без проверки). Разные векторы доставки, одинаковый результат: чужой код в контексте чужого домена.
  • **Экранировать по контексту** - HTML-контекст: `&lt;`, `&gt;`, `&amp;`. JS-контекст: \uXXXX. URL-контекст: %XX. CSS-контекст: \XX. Один алгоритм экранирования на все контексты - путь к уязвимости.
  • **CSP с nonce** - браузер блокирует любой скрипт без nonce, сгенерированного сервером на конкретный запрос. Самый надёжный рубеж. `unsafe-inline` полностью нейтрализует защиту CSP.
  • **HttpOnly + Secure на cookies** - кража cookie через XSS без этих флагов - первые 15 минут атаки. HttpOnly: cookie недоступны через JavaScript. Secure: только HTTPS. Флаги не блокируют XSS, но убирают самый простой сценарий угона сессии.
  • **dangerouslySetInnerHTML - красный флаг** - React, Angular, Vue защищают от XSS в шаблонах автоматически. Но каждый escape-hatch для raw HTML отменяет эту защиту. Если нужен rich text - DOMPurify с allowlist тегов, не доверять ни одному HTML из внешних источников.

Связанные темы

XSS живёт на пересечении браузерной модели безопасности, HTTP-заголовков и архитектуры веб-приложений:

  • SQL Injection — Та же инъекционная уязвимость - недоверенный ввод попадает в интерпретатор (SQL вместо HTML)
  • CSRF и CORS — CSRF эксплуатирует доверие сервера к браузеру жертвы; XSS даёт атакующему полный контроль над браузером для CSRF-атак
  • OWASP Top 10 — XSS исторически занимал первые строчки OWASP, в 2021 вошёл в A03:Injection
  • Безопасность API — CSP-заголовки, SameSite cookies, CORS preflight - меры защиты API от XSS-атак через браузер

Вопросы для размышления

  • Если на странице используется React и весь контент рендерится через JSX - можно ли получить XSS? При каких условиях?
  • Какой тип XSS сложнее всего обнаружить при code review и почему? Какие паттерны кода служат red flags?
  • CSP в режиме report-only не блокирует атаки, но логирует нарушения. Как использовать этот режим при переходе на строгую CSP в legacy-проекте?

Связанные уроки

  • sec-07 — SQL Injection - та же инъекция, другой контекст исполнения
  • sec-08 — CSRF эксплуатирует доверие к пользователю, XSS - к сайту
  • sec-09 — XSS входит в OWASP Top 10 как самостоятельный вектор
  • sec-17 — CSP и заголовки безопасности API напрямую закрывают XSS
  • sec-04 — Понимание криптографии помогает осмыслить цели кражи сессий
  • net-21-http-basics
XSS: Cross-Site Scripting

0

1

Войти