open atlas
↑ К треку
Наступательная безопасность RED · 02 · 02

XSS на практике

XSS бывает трёх форм — отражённая, хранимая и DOM-based — и все три заканчиваются одинаково: JavaScript атакующего исполняется в твоём origin. Разбираем, как работает каждая и почему лекарство — CSP плюс контекстное кодирование, а не фильтрация ввода.

RED Middle ◷ 15 min
Уровень
ОсновыJuniorMiddleSenior

На авторизованном пентесте тестировщик вставляет безобидный маркер — крошечный скрипт, который просто открывает диалоговое окно, — в тему тикета поддержки. На его экране ничего не происходит. Через две минуты маркер срабатывает в браузере агента, открывшего очередь тикетов, внутри административного origin компании, неся сессию этого агента. Тестировщик не прикасался к машине агента. Он написал текст в поле; приложение его сохранило, а затем отрендерило обратно в страницу как живой HTML для другого, более привилегированного пользователя. Этим маркером с тем же успехом мог быть код, читающий сессионный токен агента и отправляющий его на URL под контролем атакующего. Это хранимая XSS, и опасна она не из-за полезной нагрузки — а из-за того, что приложение решило трактовать данные как код. Весь смысл её изучения в том, чтобы понять, где принимается это решение, и перестать его принимать.

К концу урока ты будешь знать три формы XSS, что делает атакующий, когда его JavaScript исполняется в твоём origin, и почему настоящее лекарство — это CSP плюс контекстное кодирование, а не фильтрация ввода.

Сначала авторизация: это урок для защитника

Всё, что ниже, предполагает подписанный контракт с явно очерченным scope — лабораторию, CTF, программу bug-bounty с целью из списка, или собственное приложение. Внедрение скрипта в живое приложение, которым ты не владеешь, даже «просто чтобы доказать», способно скомпрометировать сессии реальных пользователей и переходит правовую черту вне зависимости от намерений. На настоящей оценке тестировщики используют безобидный, недеструктивный маркер (диалоговое окно, обращение к подконтрольному им логирующему серверу), чтобы доказать наличие точки внедрения, — но никогда код, который реально крадёт данные у настоящих пользователей. Мы изучаем механизм, чтобы защитник увидел, где именно его фреймворк отдаёт данные браузеру как код, и закрыл эту дверь. Читай это как blue-teamer, узнающий, что атакующий уже знает о твоём пути рендеринга.

Межсайтовый скриптинг (XSS) относится к категории OWASP Injection — то же семейство, что и SQL-инъекция, — и корневая причина та же: недоверенные данные интерпретируются как код неким интерпретатором. У SQLi интерпретатор — база данных; у XSS — браузер. Одно предложение, описывающее любой баг XSS: приложение взяло данные, подконтрольные атакующему, и поместило их в страницу в контексте, где браузер их исполняет. Всё остальное — детали о том, какая страница и какой контекст.

Три формы

XSS классифицируют по тому, где хранится вредоносная нагрузка и как она доходит до браузера жертвы. У форм общий итог, но резко различаются радиус поражения и место, где их чинят.

ФормаГде живёт нагрузкаКак поражается жертваРадиус пораженияГде исполняется
ОтражённаяВ запросе (URL/параметр/форма), отражается обратноЖертва должна открыть подготовленную ссылкуОдна жертва на доставленную ссылкуСервер отражает в HTML-ответ
ХранимаяСохранена в твоей БД (комментарий, профиль, тикет)Жертве достаточно открыть страницу — ссылка не нужнаКаждый зритель; может саморазмножаться (червь)Сервер рендерит сохранённые данные в HTML
DOM-basedНе покидает браузер; во фрагменте URL / состоянии клиентаЖертва открывает ссылку; клиентский JS читает и пишет это в DOMОдна жертва на ссылку; сервер может не увидеть её вовсеЦеликом на клиенте (sink вроде innerHTML)

Отражённая XSS — самая простая: сервер берёт значение из запроса — поисковый запрос, параметр ошибки — и пишет его прямо обратно в HTML-ответ без кодирования. Нагрузка атакующего нигде не хранится; она живёт в URL, который надо доставить, поэтому каждой жертве нужна подготовленная ссылка (фишинг, пост, реклама). Одна ссылка — одна жертва, но рассылается она массово без труда.

Хранимая XSS — опасная. Нагрузка записывается в твой слой хранения — комментарий, имя пользователя, тело тикета поддержки — и затем отдаётся всем, кто просматривает страницу, без всякой ссылки. Именно это делает хранимую XSS классом «червей»: нагрузка может исполняться в сессии каждого зрителя и переотправлять саму себя — ровно так червь Samy в 2005 поразил миллион профилей MySpace меньше чем за сутки. Угол привилегированного зрителя — добивающий: данные, записанные малопривилегированным атакующим, исполняются в браузере администратора, открывшего очередь модерации.

DOM-based XSS — та, что серверные защиты пропускают полностью. Здесь уязвимый код — клиентский JavaScript: страница читает подконтрольный атакующему ввод из источника (обычно location.hash, location.search, document.referrer или данные postMessage) и передаёт его в опасный sink, превращающий строки в живой DOM или код (element.innerHTML, document.write, eval, старый HTML-путь jQuery $(...)). Вредоносная строка может сидеть во фрагменте URL после #, который браузеры никогда не отправляют на сервер, — поэтому серверные логи, твой WAF и серверное кодирование её даже не видят. Уязвимость — это небезопасный поток источник→sink в браузере, и чинить её надо именно там.

Что атакующий реально получает

Как только JavaScript атакующего исполняется внутри твоего origin, он работает со всеми полномочиями, которые браузер жертвы даёт твоему сайту. Эту часть недооценивают: XSS — это не «всплывашка», это произвольное исполнение кода в контексте безопасности твоего приложения. Конкретно: скрипт может прочитать любой cookie без флага HttpOnly и любой токен, который твой SPA держит в localStorage, и слить его на сервер атакующего. Он может вызывать твой собственный API от лица жертвы — сменить ей email, перевести деньги, опубликовать контент — потому что запрос несёт сессию жертвы автоматически и исходит с твоей же страницы, проходя мимо защит same-origin и CSRF. Он может логировать клавиши, переписать DOM ради фишинга учётных данных или сделать pivot: сессия администратора, перехваченная хранимой XSS в инструменте поддержки, часто — прямой путь к целому тенанту. Сессионный cookie — главный приз именно потому, что его кража позволяет атакующему полностью пропустить аутентификацию.

Почему это работает

Почему HttpOnly на сессионном cookie не решает XSS? HttpOnly делает одну конкретную вещь: прячет cookie от document.cookie, так что скрипт не может прочитать и слить сырой токен. Это стоит делать — но JavaScript атакующего всё равно исполняется как пользователь в твоём origin, и браузер всё равно прикрепляет этот HttpOnly-cookie к каждому запросу скрипта. Поэтому вместо кражи cookie скрипт просто использует его: вызывает твой API напрямую и делает что хочет, оставаясь в сессии. HttpOnly повышает стоимость одной техники эксфильтрации; самого исполнения кода он не касается. Вот почему настоящее лекарство должно остановить запуск скрипта, а не просто спрятать от него cookie.

Лекарство: кодируй по контексту, затем CSP как страховка

Инстинкт «санитизировать ввод» — неверный первичный контроль, и зрелые инженеры знают почему: значение опасно не когда оно сохранено, а когда оно отрендерено в конкретный контекст. Одна и та же строка безобидна внутри текстового узла HTML, выламывается из атрибута HTML и катастрофична внутри блока <script> или URL javascript:. Поэтому каноническая защита — это выходное кодирование, выбранное под контекст, в который ты вставляешь: HTML-entity-кодирование для текста элемента, атрибутное кодирование (с кавычками) для атрибутов, JavaScript-кодирование для скриптовых контекстов, URL-кодирование для параметров URL. Современные фреймворки делают большую часть этого за тебя: React, Angular и Vue автоматически экранируют интерполируемые значения — поэтому XSS в таких приложениях кучкуется вокруг люков-обходов (dangerouslySetInnerHTML, [innerHTML], v-html) и вокруг DOM-sink, которые фреймворк не опосредует. Конкретно для DOM XSS лекарство — избегать строка-в-DOM sink (textContent вместо innerHTML) и, в идеале, включить Trusted Types, чтобы браузер отказывал сырым строкам у опасных sink.

Затем приходит слой, рассчитанный на то, что одну ты пропустишь, — Content-Security-Policy. Строгий CSP, запрещающий inline-скрипты и разрешающий скрипты только из источников, которые ты назвал (через nonce на запрос или хеши), означает, что даже когда внедрение приземлится, браузер откажется исполнять <script> атакующего — политика его не авторизовала. CSP — это defence-in-depth, а не замена кодированию: он не чинит баг, он сдерживает радиус поражения того бага, что ты не поймал. Зрелая позиция — и то, и другое: кодируй корректно у каждого sink, чтобы внедрения не образовывались, и поставляй CSP на основе nonce, чтобы то, что ты пропустишь, всё равно не исполнилось.

Выбери лучший вариант

React-приложение рендерит присланный пользователем markdown, конвертируя его в HTML и передавая результат в `dangerouslySetInnerHTML`. Пентест находит хранимую XSS через это. Выбери контроль, реально чинящий корневую причину.

Викторина

Нагрузка сидит во фрагменте URL после `#`, а клиентский JS читает `location.hash` и присваивает его `element.innerHTML`. Почему серверное кодирование или твой WAF этого не поймают?

Викторина

Почему каноническое лекарство от XSS — выходное кодирование, выбранное по контексту, а не один проход санитизации ввода?

Расставь шаги по порядку

Упорядочи жизненный цикл хранимой XSS по мере того, как она достигает привилегированной жертвы:

  1. 1 Малопривилегированный атакующий пишет значение со скриптом в поле, которое сохраняется (например, тело тикета)
  2. 2 Приложение сохраняет значение в базе, не отличая данные от разметки
  3. 3 Администратор открывает страницу; сервер рендерит сохранённое значение в HTML без контекстного кодирования
  4. 4 Скрипт атакующего исполняется в origin администратора и действует с его сессией
Вспомните перед уходом
  1. 01
    Назови три формы XSS и объясни, как каждая по-разному доходит до жертвы.
  2. 02
    Почему контекстное выходное кодирование — первичное лекарство, и что CSP добавляет сверх кодирования?
Итог

XSS — это инъекционный баг (недоверенные данные интерпретируются как код, и интерпретатор — браузер), и она бывает трёх форм, различающихся лишь тем, как путешествует нагрузка. Отражённая XSS возвращается эхом из подготовленной атакующим ссылки (одна жертва на ссылку). Хранимая XSS сохранена в твоей базе и отдаётся каждому зрителю без ссылки для клика — поэтому она может саморазмножаться как червь и поэтому ввод малопривилегированного атакующего может исполниться в сессии привилегированного администратора. DOM-based XSS живёт целиком на клиенте, перетекая из источника вроде location.hash в sink вроде innerHTML, часто во фрагменте URL, которого сервер никогда не видит, — поэтому серверное кодирование и WAF к ней слепы. Все три сходятся к одному итогу: JavaScript атакующего исполняется в твоём origin, с полным правом читать не-HttpOnly cookie и токены, сливать их или просто вызывать твой API от лица жертвы в сессии. Лекарство — контекстное выходное кодирование у каждого sink, а не blocklist ввода (тривиально обходится) и не один лишь HttpOnly (он только прячет cookie, пока код продолжает работать). Сверху наслаивай CSP на основе nonce как страховку, отказывающую в исполнении тому внедрению, которое ты неизбежно пропустишь. Поэтому, видя поток пользовательских данных в страницу, твой рефлекс: в какой контекст это рендерится, закодировано ли оно под этот контекст и остановил бы это мой CSP, если нет?

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.