Безопасный показ HTML-кода, созданного пользователями, – это распространенная проблема безопасности. Встроенный Sanitizer API позволяет удалять опасный контент (например, теги <script> или атрибуты on*) до того, как он попадет в DOM.
Клиентская очистка защищает страницу, на которой показывается контент. Если один и тот же пользовательский контент также отображается в других местах (другие клиенты, страницы, отображаемые сервером, или электронные письма), очистите его на сервере, а также обеспечьте глубокую защиту и убедитесь, что правила очистки сервера и клиента не конфликтуют.
1. Безопасная вставка HTML с помощью setHTML()
Основной способ использования Sanitizer API – метод setHTML() для любого Element. Этот метод выполняет синтаксический анализ, очистку и вставку за один шаг, обеспечивая очистку контента специально для контекста, в который он будет вставлен.
const untrustedHTML = `
<p>Hello!</p>
<script>alert('XSS')</script>
<img src="x" onerror="alert('XSS')">
<div onclick="doBadThing()">Click me</div>
`;
const container = document.getElementById('output');
// Sanitize and insert in one call
// The default sanitizer removes scripts and event handlers, plus other
// elements and attributes outside its built-in allowlist (such as style)
container.setHTML(untrustedHTML);
2. Использование пользовательских конфигураций дезинфектора
Вы можете настроить правила очистки, создав экземпляр Sanitizer. Это позволяет точно определить, какие элементы и атрибуты разрешены для вашего конкретного случая использования.
При использовании Element.setHTML() и Document.parseHTML() настройки Sanitizer по умолчанию удаляют все элементы и атрибуты, небезопасные с точки зрения межсайтового скриптинга. Вы не можете создать более разрешающее правило Sanitizer, которое разрешает эти элементы, но можете дополнительно ограничить то, что разрешено.
// Define a restrictive configuration that only allows the specified elements and attributes.
const config = {
elements: ["p", "b", "i", "strong", "em"],
attributes: ["class"],
replaceWithChildrenElements: ['div']
};
const mySanitizer = new Sanitizer(config);
// Apply custom sanitization
container.setHTML(untrustedHTML, { sanitizer: mySanitizer });
Элементы, не входящие в elements, удаляются вместе с контентом. Перечислите элементы-оболочки, текст которых вы хотите сохранить (например, div выше), в replaceWithChildrenElements. Это позволит удалить элемент, но сохранить его дочерние элементы.
Если вам нужен менее строгий Sanitizer, который разрешает некоторые небезопасные элементы или атрибуты, вы можете использовать собственный Sanitizer с Element.setHTMLUnsafe() и Document.parseHTMLUnsafe().
3. Анализ пользовательского ввода с помощью Document.parseHTML()
Если вам нужно проанализировать ненадежный HTML-код, не вставляя его сразу в DOM, используйте Document.parseHTML(). Этот метод возвращает объект Document, очищенный в соответствии с правилами по умолчанию или специальными правилами. Вы можете проверить или преобразовать очищенные узлы (например, чтобы создать предварительный просмотр или извлечь текст) перед их вставкой.
Переместите полученные узлы на страницу, а не сериализуйте их в строку и повторно анализируйте с помощью innerHTML, что может привести к повторному возникновению мутационного межсайтового скриптинга (mXSS).
const rawHTML = '<p>Hello <script>console.log("bad")</script></p>';
const doc = Document.parseHTML(rawHTML);
// doc.body now contains the sanitized nodes: <p>Hello </p>
preview.replaceChildren(...doc.body.childNodes);
Резервные варианты и поддержка Sanitizer API в браузерах
Если нативный Sanitizer API недоступен в целевых браузерах, загружайте библиотеку, например DOMPurify, только когда нативная поддержка отсутствует, чтобы не отправлять ее в браузеры, которым она не нужна. Используйте проверки in для обнаружения функций, чтобы сама проверка работала в старых браузерах.
let domPurifyPromise;
function loadDOMPurify() {
// Code-split with your bundler so DOMPurify is only downloaded when needed
domPurifyPromise ??= import('dompurify').then((mod) => mod.default ?? mod);
return domPurifyPromise;
}
export async function safeSetHTML(el, html) {
const elements = ['p', 'b', 'i', 'strong', 'em'];
const attributes = ['class'];
if ('setHTML' in Element.prototype) {
// setHTML() accepts a plain configuration object as the sanitizer
el.setHTML(html, { sanitizer: { elements, attributes } });
} else {
const DOMPurify = await loadDOMPurify();
el.innerHTML = DOMPurify.sanitize(html, {
ALLOWED_TAGS: [...elements, '#text'],
ALLOWED_ATTR: attributes,
// Match the native Sanitizer: strip aria-*/data-* and disallowed element content
ALLOW_ARIA_ATTR: false,
ALLOW_DATA_ATTR: false,
KEEP_CONTENT: false
});
}
}
По умолчанию DOMPurify сохраняет атрибуты aria-* и data-* (даже если указан ALLOWED_ATTR), а также текстовое содержимое удаляемых элементов, тогда как встроенный Sanitizer удаляет атрибуты aria-* и data-* и удаляет запрещенные элементы вместе с их содержимым. Чтобы DOMPurify работал так же, как нативная функция, задайте для ALLOW_ARIA_ATTR: false, ALLOW_DATA_ATTR: false и KEEP_CONTENT: false значение true, а для #text – false. В DOMPurify нет встроенного параметра, эквивалентного replaceWithChildrenElements, поэтому не используйте его в конфигурациях, для которых требуется соответствующий резервный вариант.
Если на странице используются доверенные типы (require-trusted-types-for 'script'), то при назначении строки для innerHTML будет выдаваться ошибка. Для setHTML() правила Trusted Types не нужны, но они требуются для резервного объявления. Передайте RETURN_TRUSTED_TYPE: true в DOMPurify.sanitize() и разрешите название правила dompurify в директиве CSP trusted-types.