Практическое руководство по предотвращению XSS-уязвимостей в приложениях на React и Next.js. Применимые техники для клиентской и серверной стороны, тесты и типовые ошибки — время выполнения: 45–90 минут.
0
Статья была полезной?
Комментарии (0)
Войдите или зарегистрируйтесь, чтобы оставить комментарий
Загрузка комментариев…
Что вы изучите
Где и почему возникают XSS-уязвимости в приложениях на React / Next.js.
Контроль вставки HTML через dangerouslySetInnerHTML — безопасные паттерны.
Использование DOMPurify (версия 3.1.0, релиз 2025) на клиенте и сервере.
Настройка CSP-заголовков с nginx 1.26 (релиз 2024) и через next.config.js для Next.js 14 (релиз 2026).
Методы тестирования: unit, E2E (Playwright) и автоматизированные сканеры (OWASP ZAP).
XSS (Cross-Site Scripting) возникает там, где непроверенный HTML или строковые данные попадают в DOM и интерпретируются как код. В приложениях на React типичные площадки для инъекций в 2025–2026 годах:
Использование dangerouslySetInnerHTML для рендеринга HTML от API или WYSIWYG-редакторов.
SSR: вставка данных в заранее сформированные шаблоны, когда серверная сериализация не экранирует контент.
Третьи стороны: виджеты, интеграции, которые возвращают HTML/JS.
Динамические атрибуты — href, src, data-* и innerHTML через ref-ы.
Даже при использовании React 19 (релиз 2026) компонентный рендеринг не защищает автоматически от вставки уже сгенерированного HTML. Защита — ответственность разработчика и инфраструктуры (CSP, sanitizers, тесты).
Схема потока данных от пользователя до DOM, где возможен XSS
Шаг 1: dangerouslySetInnerHTML
Команда: используйте минимально и внесите защиту. Если запроса избежать нельзя, всегда санитизируйте источник перед вставкой. Ниже пример небезопасного и безопасного использования.
// Небезопасно: вставка HTML от API без фильтрации
function RawHtml({ html }) {
return ;
}
// Использование:
//
Ожидаемый вывод (при атакующем payload = "<script>alert(1)</script>") — в небезопасном случае браузер покажет alert и выполнится скрипт, что демонстрирует XSS.
// Пример успешного вывода (опасный):
// alert(1) выполнится и откроется окно alert в браузере
Типовая ошибка: доверие содержимому API, особенно если API принимает контент от пользователей или внешних систем. Как исправить: полностью отказаться от raw HTML, либо санитизировать содержимое перед рендером.
// Безопасный паттерн: экранирование, либо строгая санитизация
import DOMPurify from 'isomorphic-dompurify'; // клиент + сервер
function SafeHtml({ html }) {
const clean = DOMPurify.sanitize(html);
return ;
}
// Пример использования:
Ожидаемый вывод после санитизации: вредоносные теги (например, <script>) удалены, HTML остаётся пригодным для отображения. Если провал теста — проверьте версию DOMPurify и серверную среду (jsdom для SSR).
// Потенциальная ошибка
dompurify not found
// Исправление: установить пакет и убедиться, что сборщик его включает
npm install isomorphic-dompurify@3.1.0
Шаг 2: DOMPurify
Команда: установка и настройка на клиенте и сервере. На 2025–2026 год стабильная связка: isomorphic-dompurify 3.1.0 и jsdom 21.0 для Node. Установка займёт ~30 секунд для локальной машины с быстрым интернетом.
Пояснение: isomorphic-dompurify автоматически использует native DOM в браузере и jsdom в Node.js. На сервере jsdom создаёт DOM-окружение для DOMPurify.
// Next.js 14 / React 19 пример компонента с SSR-совместимой санитизацией
import createDOMPurify from 'isomorphic-dompurify';
export default function Article({ html }) {
const clean = createDOMPurify().sanitize(html);
return ;
}
// getServerSideProps в Next.js
export async function getServerSideProps(ctx) {
const res = await fetch('https://api.example.com/article/123');
const data = await res.json();
return { props: { html: data.body } };
}
Ожидаемый вывод: на клиенте и на сервере HTML, очищенный по правилам DOMPurify. Время выполнения санитизации для страницы ~1–5 мс на 1 KB HTML на CPU 2.5 GHz.
// Типичная ошибка при SSR:
// Error: window is not defined
// Причина: попытка использовать браузерный DOM без jsdom
// Исправление:
npm install jsdom@21.0.0
// или использовать isomorphic-dompurify, как в примере выше
Дополнительные рекомендации: включите strict правила DOMPurify: отключите атрибуты, которые не используются, включите ALLOWED_URI_REGEXP для контроля href/src.
Команда: настройка Content-Security-Policy на уровне nginx 1.26 и через Next.js headers() API. CSP — второй уровень защиты, снижающий риск выполнения несанкционированного JavaScript даже при наличии инъекций.
Типовая ошибка: строгий CSP блокирует inline-scripts и некоторые библиотеки. Если в приложении используются inline-скрипты (analytics, third-party), вы увидите ошибки в консоли: "Refused to execute inline script because it violates the following Content Security Policy".
// Исправление 1: использовать nonce
// На сервере генерируйте nonce и вставляйте его в заголовок и в теги // Исправление 2: использовать sha256-хащирование для статичных inline-скриптов
</code></pre>
<p>Пример реализации nonce в Next.js 14 (middleware/SSR):</p>
<pre><code>// next.config.js
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'Content-Security-Policy', value: "default-src 'self'; script-src 'self' 'nonce-PLACEHOLDER'" }
]
}
];
}
};
</code></pre>
<p>Примечание: replace PLACEHOLDER на runtime-генерируемый nonce в middleware или серверной функции. Для nginx — генерация nonce требует template-движка или Lua-модуля.</p>
<img src="/images/xss-react-2.png" alt="Пример ошибки CSP в консоли браузера при блокировке inline-скрипта" />
<h2 id="kakie-sluchai-upuskayut">Какие случаи упускают?</h2>
<p>Перечисленные ниже сценарии часто остаются незамеченными в ревью кода и автоматических тестах:</p>
<ul>
<li>WebSocket-сообщения, которые применяются к DOM без валидации. Данные из WS чаще всего воспринимаются как доверенные.</li>
<li>Поля <code>data-*</code> и атрибуты URL, используемые позже как HTML, например: генерация HTML на клиенте на основе <code>data-</code> — уязвимость скрыта сложно.</li>
<li>Third-party widgets/iframes: они могут внедрять HTML в ваше пространство и обходить CSP, если разрешён <code>frame-ancestors</code> или <code>script-src</code> для CDN.</li>
<li>Логирование и мониторинг: если логи отображаются в internal UI и не экранируются, атаки через лог-файлы возможны.</li>
<li>Серверная сериализация: JSON-строки, вставленные в HTML без экранирования, особенно при генерации <code>window.__INITIAL_DATA__</code>.</li>
</ul>
<p>Рекомендация: проверяйте весь путь данных от ввода до рендера. Для критичных мест используйте принцип белого списка, а не черного.</p>
<h2 id="kak-testirovat">Как тестировать?</h2>
<p>Команда: включите unit-тесты, E2E и автоматические сканеры. Минимальный набор тестов занимает 20–60 минут настройки и даёт автоматическую проверку на CI.</p>
<ol>
<li>Unit: Jest + DOMPurify тесты на примерах payload.li>
<li>E2E: Playwright попытки вставки полезной нагрузки в поля и проверка, что скрипт не выполняется.</li>
<li>Сканеры: OWASP ZAP или Burp Suite по расписанию в CI.</li>
</ol>
<pre><code>// Пример Jest теста для компонента SafeHtml
import { render, screen } from '@testing-library/react';
import SafeHtml from './SafeHtml';
test('удаляет script теги', () => {
render(<SafeHtml html={'<p>ok</p><script>window.XSS=1'} />);
expect(screen.getByText('ok')).toBeInTheDocument();
expect(window.XSS).toBeUndefined();
});
Ожидаемый результат теста: DOM не содержит выполненных скриптов, глобальные объекты не созданы.
// Playwright E2E: попытка XSS через форму
// playwright.config.js и тест ниже
import { test, expect } from '@playwright/test';
test('form XSS attempt', async ({ page }) => {
await page.goto('http://localhost:3000');
await page.fill('#comment', "");
await page.click('#submit');
await page.waitForTimeout(500);
const val = await page.evaluate(() => window.TEST_XSS);
expect(val).toBeUndefined();
});
Типовая ошибка: тест проходит локально, но падает в CI из-за неподходящей среды (отсутствие jsdom или differen runtime). Исправление: выровнять версии Node и браузера в CI, запускать Playwright с headless=true и включать network mocking при необходимости.
// Запуск OWASP ZAP в CI
docker run -v $(pwd)/zap:/zap/wrk -t owasp/zap2docker-stable zap-baseline.py -t http://localhost:3000 -r zap_report.html
Ожидаемый вывод: отчет с обнаруженными уязвимостями. Если скан не запускается — проверьте доступность target и сетевые правила CI/CD.
Обновляйте DOMPurify при каждом релизе безопасности и не реже одного раза в квартал. В 2025—2026 годах новые правила и патчи появляются часто из‑за изменений в браузерах и новых векторов атак. В CI добавьте задачу, которая проверяет актуальность зависимостей (dependabot, Renovate). Перед обновлением прогоните unit и E2E тесты, чтобы исключить регрессии в разрешённых тегах/атрибутах.
Почему CSP не заменяет санитизацию?
CSP ограничивает исполнение контента, но не удаляет вредоносный HTML из DOM. CSP полезен как защитный слой для предотвращения исполнения inline-скриптов или загрузки внешних ресурсов, однако при нарушениях конфигурации злоумышленник может обойти частично CSP или воспользоваться слабой политикой. Поэтому CSP и sanitizers работают в паре: CSP снижает вероятность успеха, а sanitizers предотвращают саму инъекцию.
Что делать с third-party виджетами и скриптами?
Размещайте third-party скрипты на доверенных CDN и ограничивайте их в CSP. Для сложных интеграций используйте iframe с sandbox и минимальными правами. При невозможности изоляции создавайте прокси-сервис, который очищает ответ виджета, или запускайте виджет в отдельном домене (domain isolation). Логируйте активность сторонних скриптов и регулярно пересматривайте список доверенных источников.
Где тестировать XSS: на staging или production?
Основные тесты выполняйте в CI и staging. Нельзя запускать active-scans типа OWASP ZAP на production без согласования, так как они могут создать нагрузку и ложные инциденты. Production можно использовать для пассивного мониторинга и CSP reporting (report-uri/report-to), чтобы собирать данные о потенциальных попытках инъекций без активного сканирования.
Сколько слоёв защиты достаточно?
Рекомендуется минимум три слоя: white-list санитизация контента (DOMPurify), строгая CSP без разрешённых inline-скриптов и автоматизированные тесты (unit + E2E + периодические сканы). На крупных проектах добавляют Web Application Firewall (WAF) и runtime мониторинг. Количество слоёв зависит от критичности данных и доступных ресурсов, но комбинация из трёх перечисленных — минимальный профессиональный стандарт.
Комментарии (0)
Войдите или зарегистрируйтесь, чтобы оставить комментарий
Загрузка комментариев…