Manipolazione sicura del DOM con l'API Sanitizer

Jack J
Jack J

Le applicazioni gestiscono costantemente stringhe non attendibili, ma il rendering sicuro di questi contenuti come parte di un documento HTML può essere complicato. Se non presti la dovuta attenzione, potresti creare accidentalmente opportunità di cross-site scripting (XSS) che gli aggressori dannosi potrebbero sfruttare.

Per mitigare questo rischio, la nuova proposta dell'API Sanitizer mira a creare un processore robusto per le stringhe arbitrarie da inserire in sicurezza in una pagina.

// Expanded Safely !!
$div.setHTML(`<em>hello world</em><img src="" onerror=alert(0)>`, new Sanitizer())

Eseguire l'escape dell'input utente

Quando inserisci l'input utente, le stringhe di query, i contenuti dei cookie e altro ancora nel DOM, le stringhe devono essere sottoposte a escape correttamente. È necessario prestare particolare attenzione alla manipolazione del DOM con .innerHTML, dove le stringhe senza escape sono una fonte tipica di XSS.

const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
$div.innerHTML = user_input

Se esegui l'escape dei caratteri speciali HTML nella stringa di input o la espandi utilizzando .textContent, alert(0) non viene eseguito. Tuttavia, poiché <em> aggiunto dall'utente viene espanso come stringa, questo metodo non può essere utilizzato per mantenere la decorazione del testo in HTML.

La cosa migliore da fare in questo caso non è l'escape, ma la sanificazione.

Sanificare l'input utente

L'escape si riferisce alla sostituzione dei caratteri HTML speciali con entità HTML.

La sanificazione si riferisce alla rimozione delle parti semanticamente dannose (ad esempio l'esecuzione di script) dalle stringhe HTML.

Esempio

Nell'esempio precedente, <img onerror> fa sì che venga eseguito il gestore di errori, ma se il gestore onerror fosse stato rimosso, sarebbe stato possibile espanderlo in sicurezza nel DOM lasciando intatto <em>.

// XSS 🧨
$div.innerHTML = `<em>hello world</em><img src="" onerror=alert(0)>`
// Sanitized ⛑
$div.innerHTML = `<em>hello world</em><img src="">`

Per eseguire la sanificazione correttamente, è necessario analizzare la stringa di input come HTML, omettere i tag e gli attributi considerati dannosi e mantenere quelli innocui.

La specifica dell'API Sanitizer proposta mira a fornire questa elaborazione come API standard per i browser.

API Sanitizer

L'API Sanitizer viene utilizzata nel seguente modo:

const $div = document.querySelector('div')
const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
$div.setHTML(user_input, { sanitizer: new Sanitizer() }) // <div><em>hello world</em><img src=""></div>

Tuttavia, { sanitizer: new Sanitizer() } è l'argomento predefinito.

$div.setHTML(user_input) // <div><em>hello world</em><img src=""></div>

È importante notare che setHTML() è definito su Element. Essendo un metodo di Element, il contesto da analizzare è autoesplicativo (<div> in questo caso), l'analisi viene eseguita una sola volta internamente e il risultato viene espanso direttamente nel DOM.

Per ottenere il risultato della sanificazione come stringa, puoi utilizzare .innerHTML dai risultati di setHTML().

const $div = document.createElement('div')
$div.setHTML(user_input)
$div.innerHTML // <em>hello world</em><img src="">

Personalizzare con la configurazione

L'API Sanitizer è configurata per impostazione predefinita per rimuovere le stringhe che attiverebbero l'esecuzione di script. Tuttavia, puoi anche aggiungere le tue personalizzazioni al processo di sanificazione con un oggetto di configurazione.

const config = {
  allowElements: [],
  blockElements: [],
  dropElements: [],
  allowAttributes: {},
  dropAttributes: {},
  allowCustomElements: true,
  allowComments: true
};
// sanitized result is customized by configuration
new Sanitizer(config)

Le seguenti opzioni specificano come il risultato della sanificazione deve trattare l'elemento specificato.

allowElements: nomi degli elementi che lo strumento di sanificazione deve conservare.

blockElements: nomi degli elementi che lo strumento di sanificazione deve rimuovere, mantenendo i relativi elementi secondari.

dropElements: nomi degli elementi che lo strumento di sanificazione deve rimuovere, insieme ai relativi elementi secondari.

const str = `hello <b><i>world</i></b>`

$div.setHTML(str)
// <div>hello <b><i>world</i></b></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowElements: [ "b" ]}) })
// <div>hello <b>world</b></div>

$div.setHTML(str, { sanitizer: new Sanitizer({blockElements: [ "b" ]}) })
// <div>hello <i>world</i></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowElements: []}) })
// <div>hello world</div>

Puoi anche controllare se lo strumento di sanificazione consentirà o negherà gli attributi specificati con le seguenti opzioni:

  • allowAttributes
  • dropAttributes

allowAttributes e dropAttributes proprietà prevedono elenchi di corrispondenze degli attributi, ovvero oggetti le cui chiavi sono nomi di attributi e i cui valori sono elenchi di elementi di destinazione o il carattere jolly *.

const str = `<span id=foo class=bar style="color: red">hello</span>`

$div.setHTML(str)
// <div><span id="foo" class="bar" style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["span"]}}) })
// <div><span style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["p"]}}) })
// <div><span>hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["*"]}}) })
// <div><span style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({dropAttributes: {"id": ["span"]}}) })
// <div><span class="bar" style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {}}) })
// <div>hello</div>

allowCustomElements è l'opzione per consentire o negare gli elementi personalizzati. Se sono consentiti, vengono comunque applicate altre configurazioni per elementi e attributi.

const str = `<custom-elem>hello</custom-elem>`

$div.setHTML(str)
// <div></div>

const sanitizer = new Sanitizer({
  allowCustomElements: true,
  allowElements: ["div", "custom-elem"]
})
$div.setHTML(str, { sanitizer })
// <div><custom-elem>hello</custom-elem></div>

Piattaforma API

Confronto con DomPurify

DOMPurify è una libreria nota che offre funzionalità di sanificazione. La differenza principale tra l'API Sanitizer e DOMPurify è che DOMPurify restituisce il risultato della sanificazione come stringa, che devi scrivere in un elemento DOM con .innerHTML.

const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sanitized
// `<em>hello world</em><img src="">`

DOMPurify può fungere da fallback quando l'API Sanitizer non è implementata nel browser.

L'implementazione di DOMPurify presenta alcuni svantaggi. Se viene restituita una stringa, la stringa di input viene analizzata due volte, da DOMPurify e .innerHTML. Questa doppia analisi spreca tempo di elaborazione, ma può anche portare a vulnerabilità interessanti causate da casi in cui il risultato della seconda analisi è diverso dalla prima.

L'HTML richiede anche un contesto per essere analizzato. Ad esempio, <td> ha senso in <table>, ma non in <div>. Poiché DOMPurify.sanitize() accetta solo una stringa come argomento, il contesto di analisi doveva essere stimato.

L'API Sanitizer migliora l'approccio DOMPurify ed è progettata per eliminare la necessità di una doppia analisi e per chiarire il contesto di analisi.

Stato dell'API e supporto del browser

L'API Sanitizer è in fase di discussione nella procedura di standardizzazione e Chrome è in fase di implementazione.

Passaggio Stato
1. Crea explainer Completato
2. Crea bozza di specifica Completato
3. Raccogli feedback e itera sulla progettazione Completato
4. Prova dell'origine di Chrome Completato
5. Avvia Intenzione di spedire su M105

Mozilla: ritiene che questa proposta valga la pena di essere prototipata ed è in fase di implementazione attiva.

WebKit: consulta la risposta nella mailing list di WebKit.

Come abilitare l'API Sanitizer

Browser Support

  • Chrome: 146.
  • Edge: 146.
  • Firefox: 148.
  • Safari: not supported.

Chrome è in fase di implementazione dell'API Sanitizer. In Chrome 93 o versioni successive, puoi provare il comportamento abilitando il flag about://flags/#enable-experimental-web-platform-features. Nelle versioni precedenti di Chrome Canary e del canale Dev, puoi abilitarlo con --enable-blink-features=SanitizerAPI. Consulta le istruzioni su come eseguire Chrome con i flag.

Firefox

Anche Firefox implementa l'API Sanitizer come funzionalità sperimentale. Per abilitarla, imposta il flag dom.security.sanitizer.enabled su true in about:config.

Rilevamento delle funzionalità

if (window.Sanitizer) {
  // Sanitizer API is enabled
}

Feedback

Se provi questa API e hai feedback, saremo felici di riceverli. Condividi le tue opinioni sui problemi di GitHub dell'API Sanitizer e discuti con gli autori delle specifiche e le persone interessate a questa API.

Se riscontri bug o comportamenti imprevisti nell'implementazione di Chrome, segnalali. Seleziona i componenti Blink>SecurityFeature>SanitizerAPI e condividi i dettagli per aiutare gli implementatori a monitorare il problema.

Demo

Per vedere l'API Sanitizer in azione, consulta il Sanitizer API Playground di Mike West:

Riferimenti