Sanitizzazione dell'HTML non attendibile

Funzionalità web: API Sanitizer

La visualizzazione sicura di HTML generato dagli utenti è una sfida di sicurezza comune. L'API Sanitizer nativa offre un modo integrato e ottimizzato per il browser per rimuovere i contenuti pericolosi (come i tag <script> o gli attributi on*) prima che raggiungano il DOM.

La sanitizzazione lato client protegge la pagina che esegue il rendering dei contenuti. Se gli stessi contenuti generati dagli utenti vengono visualizzati anche altrove (altri client, pagine visualizzate dal server o email), esegui la sanitizzazione sul server e la difesa in profondità e assicurati che le regole di sanitizzazione del server e del client non siano in conflitto.

1. Inserimento sicuro di HTML con setHTML()

Il modo principale per utilizzare l'API Sanitizer è tramite il metodo setHTML() su qualsiasi Element. Questo metodo gestisce l'analisi, la sanitizzazione e l'inserimento in un unico passaggio, garantendo che i contenuti vengano sanitizzati in modo specifico per il contesto in cui verranno inseriti.

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. Utilizzo di configurazioni di sanitizzazione personalizzate

Puoi personalizzare le regole di sanitizzazione creando un'istanza Sanitizer. In questo modo, puoi definire esattamente quali elementi e attributi sono consentiti per il tuo caso d'uso specifico.

Quando utilizzi Element.setHTML() e Document.parseHTML(), le impostazioni predefinite di Sanitizer rimuovono tutti gli elementi e gli attributi non sicuri per XSS. Non puoi creare un Sanitizer più permissivo che consenta questi elementi, ma puoi limitare ulteriormente ciò che è consentito.

// 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 });

Gli elementi che non sono presenti in elements vengono rimossi insieme ai relativi contenuti. Elenca gli elementi wrapper di cui vuoi mantenere il testo (come div sopra) in replaceWithChildrenElements, che rimuove l'elemento, ma ne mantiene gli elementi secondari.

Se hai bisogno di un Sanitizer più permissivo che consenta alcuni elementi o attributi non sicuri per XSS, puoi utilizzare un Sanitizer personalizzato con Element.setHTMLUnsafe() e Document.parseHTMLUnsafe().

3. Analisi dell'input utente con Document.parseHTML()

Se devi analizzare codice HTML non attendibile senza inserirlo immediatamente nel DOM attivo, utilizza Document.parseHTML(). Questo metodo restituisce un Document che è stato sottoposto a sanitizzazione in base a regole predefinite o personalizzate, in modo da poter ispezionare o trasformare i nodi sottoposti a sanitizzazione (ad esempio, per creare un'anteprima o estrarre il testo) prima di inserirli.

Sposta i nodi risultanti nella pagina anziché serializzarli in una stringa e analizzarli nuovamente con innerHTML, che può reintrodurre l'XSS di mutazione (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);

Fallback e supporto dei browser per l'API Sanitizer

Se l'API Sanitizer nativa non è disponibile nei browser di destinazione, carica una libreria come DOMPurify solo quando manca il supporto nativo, per evitare di inviarla ai browser che non ne hanno bisogno. Utilizza i controlli in per il rilevamento delle funzionalità in modo che il controllo stesso funzioni nei browser meno recenti.

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
    });
  }
}

Per impostazione predefinita, DOMPurify mantiene gli attributi aria-* e data-* (anche quando è specificato ALLOWED_ATTR), nonché i contenuti testuali degli elementi che rimuove, mentre il Sanitizer nativo rimuove gli attributi aria-* e data-* e gli elementi non consentiti insieme ai relativi contenuti. Impostando ALLOW_ARIA_ATTR: false, ALLOW_DATA_ATTR: false, KEEP_CONTENT: false e consentendo #text, DOMPurify corrisponde al comportamento nativo. DOMPurify non ha un'opzione integrata equivalente a replaceWithChildrenElements, quindi evita questa opzione nelle configurazioni che richiedono un fallback corrispondente.

Se la pagina applica i Tipi attendibili (require-trusted-types-for 'script'), l'assegnazione di una stringa a innerHTML genera un errore. setHTML() non richiede una policy Trusted Types, ma il fallback sì: passa RETURN_TRUSTED_TYPE: true a DOMPurify.sanitize() e consenti il nome della policy dompurify nell'istruzione CSP trusted-types.