Bezpieczne wyświetlanie kodu HTML wygenerowanego przez użytkownika to częste wyzwanie związane z bezpieczeństwem. Natywny interfejs Sanitizer API zapewnia wbudowany, zoptymalizowany pod kątem przeglądarki sposób usuwania niebezpiecznych treści (takich jak tagi <script> lub atrybuty on*), zanim dotrą do DOM.
Oczyszczanie po stronie klienta chroni stronę, która renderuje treść. Jeśli te same treści generowane przez użytkowników są też renderowane w innym miejscu (inni klienci, strony renderowane przez serwer lub e-maile), oczyść je na serwerze, a także zastosuj wielowarstwową ochronę i upewnij się, że reguły oczyszczania serwera i klienta nie są ze sobą sprzeczne.
1. Bezpieczne wstawianie kodu HTML za pomocą setHTML()
Podstawowym sposobem korzystania z interfejsu Sanitizer API jest metoda setHTML() w dowolnym Element. Ta metoda obsługuje analizowanie, czyszczenie i wstawianie w jednym kroku, dzięki czemu treści są czyszczone pod kątem kontekstu, w którym zostaną wstawione.
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. Używanie niestandardowych konfiguracji modułu czyszczącego
Reguły usuwania informacji możesz dostosować, tworząc instancję Sanitizer. Dzięki temu możesz dokładnie określić, które elementy i atrybuty są dozwolone w Twoim przypadku.
Gdy używasz Element.setHTML() i Document.parseHTML(), domyślne ustawienia Sanitizer usuwają wszystkie elementy i atrybuty niebezpieczne dla XSS. Nie możesz utworzyć mniej restrykcyjnej zasady Sanitizer, która zezwala na te elementy, ale możesz jeszcze bardziej ograniczyć to, co jest dozwolone.
// 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 });
Elementy, które nie znajdują się w elements, są usuwane razem z zawartością. Wymień elementy opakowujące, których tekst chcesz zachować (np. div powyżej), w atrybucie replaceWithChildrenElements, który usuwa element, ale zachowuje jego elementy podrzędne.
Jeśli potrzebujesz bardziej liberalnego elementu Sanitizer, który zezwala na niektóre elementy lub atrybuty niebezpieczne dla XSS, możesz użyć niestandardowego elementu Sanitizer z Element.setHTMLUnsafe() i Document.parseHTMLUnsafe().
3. Analizowanie danych wejściowych użytkownika za pomocą Document.parseHTML()
Jeśli chcesz przeanalizować niezaufany kod HTML bez natychmiastowego wstawiania go do aktywnego modelu DOM, użyj Document.parseHTML(). Ta metoda zwraca obiekt Document, który został oczyszczony zgodnie z regułami domyślnymi lub niestandardowymi, dzięki czemu możesz sprawdzić lub przekształcić oczyszczone węzły (np. aby utworzyć podgląd lub wyodrębnić tekst) przed ich wstawieniem.
Przenieś wynikowe węzły na stronę, zamiast serializować je do ciągu znaków i ponownie analizować za pomocą innerHTML, co może spowodować ponowne wystąpienie mutacji XSS (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);
Zastępcze rozwiązania i obsługa interfejsu Sanitizer API w przeglądarkach
Jeśli natywny interfejs Sanitizer API nie jest dostępny w docelowych przeglądarkach, wczytaj bibliotekę, np. DOMPurify, tylko wtedy, gdy brakuje natywnej obsługi, aby uniknąć dostarczania jej do przeglądarek, które jej nie potrzebują. Używaj testów in do wykrywania funkcji, aby sam test działał w starszych przeglądarkach.
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
});
}
}
Domyślnie DOMPurify zachowuje atrybuty aria-* i data-* (nawet jeśli określono ALLOWED_ATTR) oraz treść tekstową usuwanych elementów, podczas gdy natywny Sanitizer usuwa atrybuty aria-* i data-* oraz niedozwolone elementy wraz z ich zawartością. Ustawienie ALLOW_ARIA_ATTR: false, ALLOW_DATA_ATTR: false, KEEP_CONTENT: false i zezwolenie na #text sprawia, że DOMPurify działa tak samo jak natywna funkcja. DOMPurify nie ma wbudowanej opcji odpowiadającej replaceWithChildrenElements, więc unikaj tej opcji w konfiguracjach, które wymagają dopasowania zastępczego.
Jeśli strona wymusza używanie typów zaufanych (require-trusted-types-for 'script'), przypisanie ciągu znaków do innerHTML powoduje błąd. setHTML() nie wymaga zasady Trusted Types, ale wymaga jej kreacja zastępcza: przekaż RETURN_TRUSTED_TYPE: true do DOMPurify.sanitize() i zezwól na nazwę zasady dompurify w dyrektywie CSP trusted-types.