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 gestiscono sempre stringhe non attendibili, ma il rendering sicuro di questi contenuti all'interno di un documento HTML può essere complicato. Senza sufficiente attenzione, è facile creare accidentalmente opportunità per il cross-site scripting (XSS) che potrebbero essere sfruttate da utenti malintenzionati.

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 ne spiega l'utilizzo.

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

Escaping dell'input 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. È necessario prestare particolare attenzione alla manipolazione del DOM tramite .innerHTML, in cui le stringhe non sfuggite sono una fonte tipica di XSS.

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

Se inserisci caratteri di escape per i caratteri speciali HTML nella stringa di input sopra 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

Per interpretazione letterale si intende la sostituzione dei 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 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 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 setHTML() è definito in Element. Poiché è un metodo di Element, il contesto da analizzare è autoesplicativo (<div> in questo caso), 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="">

Personalizzazione tramite configurazione

L'API Sanitizer è configurata per impostazione predefinita per rimuovere le stringhe che attiverebbero l'esecuzione dello script. Tuttavia, puoi anche aggiungere le tue personalizzazioni alla procedura 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 sanitizzazione deve trattare l'elemento specificato.

allowElements: i nomi degli elementi che lo strumento di convalida 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 convalida deve consentire o negare attributi specifici con le seguenti opzioni:

  • allowAttributes
  • dropAttributes

Le proprietà allowAttributes e dropAttributes richiedono elenchi di corrispondenze degli attributi, ovvero oggetti le cui chiavi sono i nomi degli attributi e i valori sono elenchi di elementi target o la wildcard *.

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 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>

API surface

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 stringa, che devi 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ò essere utilizzato come opzione di 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 richiede tempo di elaborazione, ma può anche portare a vulnerabilità interessanti causate da casi in cui il risultato della seconda analisi è diverso dal primo.

Per l'analisi dell'HTML è necessario anche il 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 di analisi.

L'API Sanitizer migliora l'approccio di 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

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. Creare una bozza delle specifiche Completato
3. Raccogli feedback e esegui l'iterazione sul design Completato
4. Prova dell'origine di Chrome Completato
5. Lancio Intent to Ship su M105

Mozilla: ritiene che questa proposta valga la pena di essere prototipizzata e la sta implementando attivamente.

WebKit: vedi la risposta nella mailing list WebKit.

Come abilitare l'API Sanitizer

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

Chrome

Chrome è in fase di implementazione dell'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 del canale Dev e Canary di Chrome, puoi attivarlo 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 attivarla, imposta il flag dom.security.sanitizer.enabled su true in about:config.

Rilevamento di funzionalità

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

Feedback

Se provi questa API e hai un feedback, ci farebbe piacere ricevere il tuo parere. 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, segnala un bug. 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, dai un'occhiata al Playground Sanitizer API di Mike West:

Riferimenti


Foto di Towfiqu barbhuiya su Unsplash.