Manipolazione sicura del DOM con l'API Sanitizer

Lo scopo della nuova API Sanitizer è creare un elaboratore affidabile per inserire in modo sicuro stringhe arbitrarie 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 proposta della nuova API Sanitizer mira a creare un elaboratore robusto per l'inserimento sicuro di stringhe arbitrarie 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 inserisci input utente, stringhe di query, contenuti dei cookie e così via nel DOM, le stringhe devono essere inserite correttamente. È 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 qui non è eseguire la fuga, ma eseguire la sanitizzazione.

Sanitizzazione dell'input utente

La differenza tra escape e convalida

Per interpretazione letterale si intende la sostituzione dei caratteri HTML speciali con entità HTML.

Per convalida si intende la rimozione di parti semanticamente dannose (ad esempio 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 fosse stato rimosso, sarebbe stato possibile espanderlo in sicurezza nel DOM lasciando <em> invariato.

// 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 come di seguito.

$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 sanitizzazione sotto forma di 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="">

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 libreria ben nota che offre funzionalità di pulizia. 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 alcuni 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 un doppio parsing e per chiarire il contesto di analisi.

Stato dell'API e supporto del browser

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

Passaggio Stato
1. Creare un'animazione esplicativa 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: consulta la risposta nella mailing list di WebKit.

Come abilitare l'API Sanitizer

Attivazione tramite about://flags o opzione CLI

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 anche l'API Sanitizer come funzionalità 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 la tua opinione sui problemi di GitHub dell'API Sanitizer e parla con gli autori delle specifiche e con 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 Sandbox dell'API Sanitizer di Mike West:

Riferimenti


Foto di Towfiqu barbhuiya su Unsplash.