Manipolazione sicura del DOM con l'API Sanitizer

La nuova API Sanitizer mira a costruire un processore robusto per le stringhe arbitrarie da inserire in modo sicuro in una pagina.

Jack J
Jack J

Le applicazioni utilizzano sempre stringhe non attendibili, ma eseguire il rendering sicuro dei contenuti come parte di un documento HTML può essere complicato. Senza un'attenzione sufficiente, è facile creare accidentalmente opportunità di cross-site scripting (XSS) che i malintenzionati possono sfruttare.

Per ridurre questo rischio, la nuova proposta relativa all'API Sanitizer mira a costruire un processore robusto per le stringhe arbitrarie da inserire in modo sicuro in una pagina. Questo articolo introduce l'API e spiega il suo utilizzo.

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

Sequenza di escape nell'input dell'utente

Quando si inseriscono nel DOM l'input utente, le stringhe di query, i contenuti dei cookie e così via, è necessario che i caratteri di escape siano corretti. Presta particolare attenzione alla manipolazione del DOM tramite .innerHTML, dove le stringhe senza caratteri di escape sono una tipica fonte 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 precedente o la espandi utilizzando .textContent, alert(0) non verrà eseguito. Tuttavia, poiché <em> aggiunto dall'utente viene espanso anche come stringa così com'è, questo metodo non può essere utilizzato per mantenere la decorazione del testo in HTML.

La cosa migliore da fare in questo caso è non fuggire, ma sanificare.

Eliminazione dell'input utente

La differenza tra fuga e sanificazione

L'interpretazione letterale si riferisce alla sostituzione di caratteri HTML speciali con entità HTML.

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

Esempio

Nell'esempio precedente, <img onerror> causa l'esecuzione del gestore degli errori, ma se il gestore onerror venisse rimosso, sarebbe possibile espanderlo in modo sicuro nel DOM senza modificare <em>.

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

Per una corretta sanificazione, è 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 questo tipo di 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. Quindi può essere proprio come sotto.

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

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

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

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

Personalizza tramite configurazione

L'API Sanitizer è configurata per impostazione predefinita in modo da rimuovere le stringhe che attivano l'esecuzione dello script. Tuttavia, puoi anche aggiungere le tue personalizzazioni al processo di sanificazione tramite 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 in che modo il risultato della sanificazione deve trattare l'elemento specificato.

allowElements: nomi degli elementi che il disinfettante deve conservare.

blockElements: nomi degli elementi che il disinfettante dovrebbe rimuovere, mantenendo i figli.

dropElements: nomi degli elementi che il disinfettante deve rimuovere, insieme ai relativi figli.

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 il sanitizer consentirà o nega gli attributi specificati con le seguenti opzioni:

  • allowAttributes
  • dropAttributes

Le proprietà allowAttributes e dropAttributes prevedono elenchi di corrispondenze degli attributi, ovvero oggetti le cui chiavi sono nomi di attributi, mentre i valori sono elenchi di elementi target 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 consentiti, verranno 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 nota libreria che offre funzionalità di sanificazione. La differenza principale tra l'API Sanitizer e DOMPurify è che DOMPurify restituisce il risultato della sanitizzazione come una stringa, che è necessario scrivere in un elemento DOM tramite .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 riserva quando l'API Sanitizer non è implementata nel browser.

L'implementazione di DOMPurify presenta un paio di 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 generare vulnerabilità interessanti causate da casi in cui il risultato della seconda analisi è diverso dalla prima.

Per analizzare il codice HTML occorre anche contesto. Ad esempio, <td> ha senso in <table>, ma non in <div>. Poiché DOMPurify.sanitize() accetta solo una stringa come argomento, è stato necessario indovinare il contesto dell'analisi.

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

Stato delle API e supporto del browser

Nell'ambito del processo di standardizzazione è in corso una discussione dell'API Sanitizer e Chrome la sta implementando.

Passaggio Stato
1. Crea messaggio esplicativo Completato
2. Crea bozza delle specifiche Completato
3. Raccogli feedback e ottimizza la progettazione Completato
4. Prova dell'origine di Chrome Completato
5. Lancio Intenzione di spedizione su M105

Mozilla: ritiene che questa proposta valga la pena eseguire la prototipazione e la sta implementando attivamente.

WebKit: vedi la risposta nella mailing list WebKit.

Come abilitare l'API Sanitizer

Supporto dei browser

  • Chrome: non supportato.
  • Edge: non supportato.
  • Firefox: dietro una bandiera.
  • Safari: non supportato.

Origine

Abilitazione tramite about://flags o opzione interfaccia a riga di comando

Chrome

Chrome sta implementando l'API Sanitizer. In Chrome 93 o versioni successive, puoi provare il comportamento attivando il flag about://flags/#enable-experimental-web-platform-features. Nelle versioni precedenti di Chrome Canary e del canale Dev, puoi abilitarlo tramite --enable-blink-features=SanitizerAPI e provarlo subito. Consulta le istruzioni su come eseguire Chrome con i flag.

Firefox

Firefox implementa inoltre l'API Sanitizer come funzione sperimentale. Per abilitarlo, imposta il flag dom.security.sanitizer.enabled su true in about:config.

Rilevamento delle caratteristiche

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

Feedback

Se provi questa API e hai dei feedback, ci piacerebbe conoscere la tua opinione. 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 rilevi bug o comportamenti imprevisti nell'implementazione di Chrome, segnala un bug per segnalarlo. Seleziona i componenti Blink>SecurityFeature>SanitizerAPI e condividi i dettagli per aiutare gli implement a monitorare il problema.

Demo

Per vedere l'API Sanitizer in azione, dai un'occhiata al Playground Sanitizer API di Mike West:

Riferimenti


Foto di Towfiqu barbhuiya su Unsplash.