Het veilig weergeven van door gebruikers gegenereerde HTML is een veelvoorkomende beveiligingsuitdaging. De native Sanitizer API biedt een ingebouwde, voor de browser geoptimaliseerde manier om gevaarlijke content (zoals <script>-tags of on*-kenmerken) te verwijderen voordat deze de DOM bereikt.
Opschoning aan de clientzijde beschermt de pagina die de content weergeeft. Als dezelfde content van gebruikers ook ergens anders wordt weergegeven (andere clients, door de server gerenderde pagina's of e-mails), moet je deze zowel op de server als in de diepte opschonen en zorgen dat de opschoningsregels van de server en de client niet conflicteren.
1. HTML veilig invoegen met setHTML()
De primaire manier om de Sanitizer API te gebruiken is via de methode setHTML() op een Element. Deze methode verwerkt parseren, opschonen en invoegen in één stap, zodat content specifiek wordt opgeschoond voor de context waarin deze wordt ingevoegd.
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. Aangepaste opschoningsconfiguraties gebruiken
Je kunt de opschoningsregels aanpassen door een Sanitizer-instantie te maken. Zo kunt u precies definiëren welke elementen en kenmerken zijn toegestaan voor uw specifieke toepassing.
Als je Element.setHTML() en Document.parseHTML() gebruikt, worden met de standaardinstellingen van Sanitizer alle elementen en kenmerken verwijderd die niet veilig zijn voor XSS. Je kunt geen minder beperkende Sanitizer maken die deze elementen toestaat, maar je kunt wel verder beperken wat is toegestaan.
// 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 });
Elementen die niet in elements staan, worden samen met hun content verwijderd. Vermeld wrapper-elementen waarvan je de tekst wilt behouden (zoals div hierboven) in replaceWithChildrenElements. Hiermee wordt het element verwijderd, maar worden de onderliggende elementen behouden.
Als je een meer tolerante Sanitizer nodig hebt die bepaalde XSS-onveilige elementen of kenmerken toestaat, kun je een aangepaste opschoner gebruiken met Element.setHTMLUnsafe() en Document.parseHTMLUnsafe().
3. Gebruikersinvoer parseren met Document.parseHTML()
Als je niet-vertrouwde HTML wilt parseren zonder deze onmiddellijk in de live DOM in te voegen, gebruik je Document.parseHTML(). Deze methode retourneert een Document die is opgeschoond volgens standaard- of aangepaste regels, zodat je de opgeschoonde nodes kunt inspecteren of transformeren (bijvoorbeeld om een voorbeeld te maken of tekst te extraheren) voordat je ze invoegt.
Verplaats de resulterende nodes naar de pagina in plaats van ze te serialiseren naar een tekenreeks en ze opnieuw te parseren met innerHTML, waardoor mutatie-XSS (mXSS) opnieuw kan worden geïntroduceerd.
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);
Fallbacks en browserondersteuning voor de Sanitizer API
Als de native Sanitizer API niet beschikbaar is in je doelbrowsers, laad je een bibliotheek zoals DOMPurify alleen als native ondersteuning ontbreekt. Zo voorkom je dat je deze naar browsers stuurt die deze niet nodig hebben. Gebruik in-controles voor functiedetectie, zodat de controle zelf werkt in oudere browsers.
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
});
}
}
Standaard behoudt DOMPurify de kenmerken aria-* en data-* (zelfs als ALLOWED_ATTR is gespecificeerd) en de tekstcontent van elementen die worden verwijderd, terwijl de systeemeigen Sanitizer de kenmerken aria-* en data-* verwijdert en niet-toegestane elementen samen met hun content verwijdert. Als je ALLOW_ARIA_ATTR: false, ALLOW_DATA_ATTR: false en KEEP_CONTENT: false instelt en #text toestaat, komt DOMPurify overeen met het systeemeigen gedrag. DOMPurify heeft geen ingebouwde optie die gelijk is aan replaceWithChildrenElements. Vermijd die optie dus in configuraties die een overeenkomende fallback nodig hebben.
Als de pagina Vertrouwde Typen (require-trusted-types-for 'script') afdwingt, wordt er een fout gegenereerd als je een tekenreeks toewijst aan innerHTML. setHTML() heeft geen beleid voor vertrouwde typen nodig, maar de fallback wel: geef RETURN_TRUSTED_TYPE: true door aan DOMPurify.sanitize() en sta de beleidsnaam dompurify toe in je trusted-types CSP-instructie.