Перейти к основному содержимому

Cross-Site Scripting (XSS)

XSS-атаки позволяют выполнить произвольный JS-код в контексте пользователя, что ведёт к краже токенов, перехвату сессий и подмене контента. Современные фреймворки экранируют данные по умолчанию — этот защитный механизм обходить без крайней необходимости FORBIDDEN.

Правила

MUST

  • Если рендеринг HTML необходим (CMS, WYSIWYG, Markdown) — санитизация выполняется непосредственно перед рендерингом
  • Использовать только проверенные библиотеки санитизации:
    • dompurify — для клиентского рендеринга
    • isomorphic-dompurify — для SSR-приложений
  • Применять принцип whitelist (явное указание разрешенных тегов и атрибутов)

FORBIDDEN

  • Использование dangerouslySetInnerHTML без предварительной санитизации
  • Присвоение недоверенных данных в innerHTML через ref или DOM API без санитизации
  • Динамическое формирование HTML-строк с пользовательскими данными
  • Самостоятельная реализация санитизации (в т.ч. через Regular Expressions)
    • Количество векторов атаки слишком велико для ручной обработки

Примеры

Запрещённые практики

// ❌ FORBIDDEN: Вставка сырого HTML без санитизации
function CommentBody({ html }: { html: string }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

// ❌ FORBIDDEN: Использование innerHTML через ref
function CommentBody({ html }: { html: string }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (ref.current) {
ref.current.innerHTML = html; // Опасно!
}
}, [html]);
return <div ref={ref} />;
}

// ❌ FORBIDDEN: Динамическое формирование HTML с пользовательскими данными
function UserGreeting({ name }: { name: string }) {
const html = `<h1>Hello, ${name}!</h1>`; // Опасно!
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

// ❌ FORBIDDEN: Самостоятельная санитизация через regex
function sanitizeHTML(html: string) {
return html
.replace(/<script/gi, '') // Недостаточно!
.replace(/on\w+=/gi, ''); // Обходится легко!
}

Правильный подход

// ✅ MUST: Санитизация с dompurify
import DOMPurify from 'dompurify';

function CommentBody({ html }: { html: string }) {
const sanitizedHtml = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a'],
ALLOWED_ATTR: ['href']
});
return <div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />;
}

// ✅ MUST: isomorphic-dompurify для SSR
import DOMPurify from 'isomorphic-dompurify';

export function ArticleContent({ html }: { html: string }) {
const sanitized = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['h1','h2','h3','p','br','strong','em','ul','ol','li','a','code','pre'],
ALLOWED_ATTR: ['href', 'target', 'rel']
});
return <article dangerouslySetInnerHTML={{ __html: sanitized }} />;
}

// ✅ Лучше всего: React без обхода экранирования
function UserGreeting({ name }: { name: string }) {
return <h1>Hello, {name}!</h1>; // React экранирует автоматически
}

Whitelist конфигурация

// Базовый текст (комментарии)
const BASIC_TEXT_CONFIG = {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em'],
ALLOWED_ATTR: [],
};

// Форматированный текст (статьи)
const RICH_TEXT_CONFIG = {
ALLOWED_TAGS: [
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'p',
'br',
'strong',
'em',
'u',
's',
'ul',
'ol',
'li',
'a',
'img',
'blockquote',
'code',
'pre',
],
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'target', 'rel'],
};

URL-контекст

Экранирование HTML не защищает от XSS через URL-атрибуты.

MUST

  • Для href, src, action с пользовательскими данными — проверять протокол по whitelist
  • Разрешены только схемы: http:, https:, mailto:
  • Не доверять URL из query-параметров, postMessage или Web Storage без валидации

FORBIDDEN

  • Схемы javascript:, data:, vbscript: в URL-атрибутах
// ❌ FORBIDDEN: Прямое использование URL из внешних данных
function UserLink({ url }: { url: string }) {
return <a href={url}>Click</a>; // Опасно! Может быть javascript:alert(1)
}

// ✅ MUST: Валидация протокола
const ALLOWED_PROTOCOLS = ['http:', 'https:', 'mailto:'];

function sanitizeUrl(url: string): string {
try {
const parsed = new URL(url);
if (!ALLOWED_PROTOCOLS.includes(parsed.protocol)) {
return '#'; // Безопасный fallback
}
return url;
} catch {
return '#'; // Невалидный URL
}
}

function UserLink({ url }: { url: string }) {
return <a href={sanitizeUrl(url)}>Click</a>;
}

Trusted Types API

SHOULD

Trusted Types — браузерный API, который запрещает передачу необработанных строк в опасные DOM-функции (innerHTML, document.write, eval) на уровне runtime.

Включается через CSP-директиву:

Content-Security-Policy: require-trusted-types-for 'script'

DOMPurify поддерживает создание Trusted Types из коробки:

// ✅ SHOULD: DOMPurify + Trusted Types
const sanitized = DOMPurify.sanitize(input, { RETURN_TRUSTED_TYPE: true });
element.innerHTML = sanitized; // Безопасно: Trusted Type, не строка

Ограничение: Trusted Types поддерживаются не во всех браузерах — проверяйте совместимость перед внедрением (caniuse: Trusted Types).