Manipolazione sicura del DOM con l'API Sanitizer

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

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. Senza la dovuta attenzione, è facile creare inavvertitamente opportunità di cross-site scripting (XSS) che gli aggressori malintenzionati possono sfruttare.

Per mitigare questo rischio, la nuova proposta di API Sanitizer mira a creare un processore solido 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="" onerro>r=alert(0)`, new Sanitizer())

Escape dell'input utente

Quando inserisci input dell'utente, stringhe di query, contenuti dei cookie e così via nel DOM, le stringhe devono essere sottoposte a escape correttamente. Particolare attenzione deve essere prestata alla manipolazione del DOM tramite .innerHTML, dove le stringhe non sottoposte a escape sono una tipica fonte di XSS.

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

Se inserisci caratteri speciali HTML di escape nella stringa di input precedente o la espandi utilizzando .textContent, alert(0) non verrà eseguito. Tuttavia, poiché anche <em> aggiunto dall'utente viene espanso 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 è l'escape, ma la sanificazione.

Sanificazione dell'input utente

Differenza tra escape e sanificazione

L'escape consiste nel sostituire i caratteri HTML speciali con entità HTML.

La sanitizzazione 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 lasciando intatto <em>.

// XSS 🧨
$div.innerHTML = `<em>hello world</em><img src="" onerro>r=alert(0)`
// Sanitized ⛑
$div.inn<er>HTML = `emh<ell><o 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_i<np>ut = `emhel<lo ><world/emimg src="">; onerror=alert(0)`
$div.setHTML(user_input, { sanitizer: new <San><it>izer() }) /</ d><ivemhello ><worl>d/emimg 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=&q><uot;>&quot;/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.inner<HT>ML // emhel<lo ><world/emim>g src=""

Personalizzazione tramite 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 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 come il risultato della sanificazione deve trattare l'elemento specificato.

allowElements: Nomi degli elementi che il sanificatore deve conservare.

blockElements: nomi degli elementi che il sanificatore deve rimuovere, mantenendo i relativi elementi secondari.

dropElements: i nomi degli elementi che il sanificatore 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" <]})> })
//< >divhe<ll><o bw>orld/b/div

$div.setHTML(str, { sanitizer: new Sanitizer({blockElements: [ &quo<t;b>"< >]}) }<)<>/span>
<// d>ivhello iworld/i/div

$div.setHTML(str, { sanitizer: new Sanitizer({allowE<lem>ents: []}) <})
/>/ divhello world/div

Puoi anche controllare se il sanificatore 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 i cui valori sono elenchi di elementi di destinazione o il carattere jolly *.

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

$div.setHTM<L(s><tr)
// divspan id="foo" class=&quo>t;bar<"><; st>yle="color: red"hello/span/div

$div.setHTML(str, { sanitizer: new Sanitizer({allow<Att><ributes: {"style&q>uot;:< [&qu><ot;s>pan"]}}) })
// divspan style="color: red"hello/span/div

$div.setHTML(str, <{ s><anit>izer:< new ><Sani>tizer({allowAttributes: {"style": ["p"]}}) })
// divspanhello/span/div<

$><div.setHTML(str, { sani>tizer<: new>< San>itizer({allowAttributes: {"style": ["*"]}}) })
// divspan style="<;co><lor: red"hello/span/div

$div.>setHT<ML(st><r, {> sanitizer: new Sanitizer({dropAttributes: {"id": ["span"<;]}>}) })<
// >divspan class="bar" style="color: red"hello/span/div

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

allowCustomElements è l'opzione per consentire o negare gli elementi personalizzati. Se sono consentite, 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 >})
//< divcustom-e><lemh>ello/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 tramite .innerHTML.

const user_input = `<em>hello world</em><img src="" onerro>r=alert(0)`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sani<ti>zed
// `emh<ell><o 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.

Anche l'HTML deve essere analizzato in base al contesto. 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 un doppio parsing 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. Creare una spiegazione Completato
2. Creare una bozza di specifiche Completato
3. Raccogliere feedback e perfezionare il design Completato
4. Prova dell'origine di Chrome Completato
5. Avvia Intent to Ship on M105

Mozilla: ritiene che questa proposta valga la pena di creare un prototipo e la sta implementando attivamente.

WebKit: vedi la risposta nella mailing list di WebKit.

Come abilitare l'API Sanitizer

Browser Support

  • Chrome: not supported.
  • Edge: not supported.
  • Firefox Technology Preview: supported.
  • Safari: not supported.

Attivazione tramite about://flags o opzione CLI

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 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 attivarlo, 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 un feedback, ci farebbe piacere riceverlo. Condividi le tue opinioni sui problemi di GitHub dell'API Sanitizer e discuti 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, consulta il playground dell'API Sanitizer di Mike West:

Riferimenti


Foto di Towfiqu barbhuiya su Unsplash.