Информационная безопасность
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 при выводе. `<` становится `<`, `>` становится `>`, `"` становится `"`. Браузер отображает символы, но не интерпретирует как 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-контекст: `<`, `>`, `&`. 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