Manipolazione sicura del DOM con l'API Sanitizer

La nuova API Sanitizer mira a creare un processore solido per l'inserimento sicuro in una pagina di stringhe arbitrarie.

Jack J
Jack J

Le applicazioni trattano sempre stringhe non attendibili, ma eseguire un rendering sicuro di quei contenuti all'interno di un documento HTML può essere complicato. Senza cure sufficienti, è facile creare accidentalmente opportunità di cross-site scripting (XSS) che possono essere sfruttate da malintenzionati.

Per ridurre questo rischio, la nuova proposta API Sanitizer mira a creare un processore solido per l'inserimento sicuro in stringhe arbitrarie in una pagina. Questo articolo illustra l'API e ne spiega l'utilizzo.

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

Escape dell'input utente

Quando inserisci nel DOM l'input utente, le stringhe di query, i contenuti dei cookie e così via, le stringhe devono contenere i caratteri di escape corretti. Presta particolare attenzione alla manipolazione del DOM tramite .innerHTML, dove le stringhe senza caratteri di 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 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 il testo decorativo in HTML.

La cosa migliore da fare qui non è l'escape, ma la sanitizzazione.

Eliminazione dell'input utente in corso...

La differenza tra escape e sanitizzazione

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

Per sanitizzazione si intende la 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 fosse rimosso, sarebbe 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 eliminare correttamente i dati, è 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 un'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. Può essere proprio come spiegato di seguito.

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

Vale la pena notare che setHTML() è definito su 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 sanitizzazione 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="">

Personalizzazione tramite configurazione

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

allowElements: nomi degli elementi che il sanitizer deve conservare.

blockElements: nomi degli elementi che il disinfettante deve 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 negherà 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 e valori che 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 sono consentiti, vengono comunque applicate altre configurazioni di 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 molto conosciuta che offre funzionalità di sanitizzazione. La differenza principale tra l'API Sanitizer e DOMPurify è che DOMPurify restituisce il risultato della sanitizzazione sotto forma di 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ò fungere da riserva quando l'API Sanitizer non è implementata nel browser.

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

Anche il codice HTML richiede l'analisi del contesto. Ad esempio, <td> ha senso in <table>, ma non in <div>. Poiché DOMPurify.sanitize() prende 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

L'API Sanitizer è in fase di discussione nel processo di standardizzazione e Chrome sta per implementarla.

Passaggio Stato
1. Crea messaggio esplicativo Completato
2. Crea bozza di specifica Completato
3. Raccogli feedback e ottimizza il design Completato
4. Prova dell'origine di Chrome Completato
5. Lancio Intenzione di spedire sulla M105

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

WebKit: vedi la risposta nella mailing list WebKit.

Come abilitare l'API Sanitizer

Supporto dei browser

  • x
  • x
  • x

Fonte

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

Chrome

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

Firefox

Firefox implementa l'API Sanitizer anche come funzionalità sperimentale. Per abilitarlo, 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 dei feedback, ci piacerebbe conoscere la tua opinione. Condividi le tue opinioni sui problemi relativi a 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 per segnalarlo. Seleziona i componenti Blink>SecurityFeature>SanitizerAPI e condividi i dettagli per aiutare gli autori dell'implementazione a monitorare il problema.

Demo

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

Riferimenti


Foto di Towfiqu barbhuiya su Unsplash.