La creazione di un'esperienza completa sul web di oggi comporta quasi inevitabilmente l'incorporamento di componenti e contenuti su cui non hai un controllo reale. I widget di terze parti possono aumentare il coinvolgimento e svolgere un ruolo fondamentale nell'esperienza utente complessiva, mentre i contenuti generati dagli utenti sono a volte ancora più importanti dei contenuti nativi di un sito. Evitare entrambi non è un'opzione, ma entrambi aumentano il rischio che sul tuo sito possa accadere qualcosa di spiacevole. Ogni widget incorporato, ogni annuncio, ogni widget dei social media, è un potenziale vettore d'attacco per chi ha intenzioni dannose:
La Content Security Policy (CSP) può mitigare i rischi associati a entrambi i tipi di contenuti, offrendoti la possibilità di inserire nella lista consentita origini attendibili specifiche di script e altri contenuti. Si tratta di un passo importante nella giusta direzione, ma vale la pena notare che la protezione offerta dalla maggior parte delle istruzioni CSP è binaria: la risorsa è consentita o meno. In alcuni casi, sarebbe utile dire "Non sono sicuro di considerare attendibile questa fonte di contenuti, ma è così bella! Incorporalo, per favore, browser, ma non lasciare che danneggi il mio sito".
Privilegio minimo
In sostanza, stiamo cercando un meccanismo che ci consenta di concedere ai contenuti che incorporiamo solo il livello minimo di funzionalità necessario per svolgere il loro lavoro. Se un widget non ha bisogno di aprire una nuova finestra, la rimozione dell'accesso a window.open non può causare danni. Se non richiede Flash, la disattivazione del supporto dei plug-in non dovrebbe essere un problema. Siamo il più sicuri possibile se seguiamo il principio del privilegio minimo e blocchiamo ogni singola funzionalità che non è direttamente pertinente alle funzionalità che vorremmo utilizzare. Il risultato è che non dobbiamo più fidarci alla cieca del fatto che alcuni contenuti incorporati non approfitteranno di privilegi che non dovrebbero utilizzare. Semplicemente, non avrà accesso alla funzionalità.
Gli elementi iframe sono il primo passo verso un buon framework per una soluzione di questo tipo.
Il caricamento di un componente non attendibile in un iframe fornisce una misura di separazione tra l'applicazione e i contenuti che vuoi caricare. I contenuti con frame non avranno accesso al DOM della tua pagina o ai dati che hai archiviato localmente, né potranno essere disegnati in posizioni arbitrarie della pagina; l'ambito è limitato al contorno del frame. Tuttavia, la separazione non è veramente efficace. La pagina contenuta ha ancora una serie di opzioni per comportamenti fastidiosi o dannosi: video con riproduzione automatica, plug-in e popup sono la punta dell'iceberg.
L'attributo sandbox dell'elemento iframe ci fornisce esattamente ciò di cui abbiamo bisogno per rafforzare le limitazioni sui contenuti con frame. Possiamo
indicare al browser di caricare i contenuti di un frame specifico in un ambiente
con privilegi limitati, consentendo solo il sottoinsieme di funzionalità necessarie per svolgere
il lavoro richiesto.
Fidati, ma verifica
Il pulsante "Tweet" di Twitter è un ottimo esempio di funzionalità che può essere incorporata in modo più sicuro nel tuo sito tramite una sandbox. Twitter ti consente di incorporare il pulsante tramite un iframe con il seguente codice:
<iframe src="https://platform.twitter.com/widgets/tweet_button.html"
style="border: 0; width:130px; height:20px;"></iframe>
Per capire cosa possiamo bloccare, esaminiamo attentamente le funzionalità richieste dal pulsante. Il codice HTML caricato nel frame esegue un po' di JavaScript dai server di Twitter e genera un popup con un'interfaccia per i tweet quando viene selezionato. Questa interfaccia deve accedere ai cookie di Twitter per collegare il tweet all'account corretto e deve essere in grado di inviare il modulo per i tweet. Questo è tutto: il frame non deve caricare plug-in, non deve navigare nella finestra di primo livello o in una serie di altre funzionalità. Poiché non ha bisogno di questi privilegi, rimuoviamoli eseguendo il sandboxing dei contenuti del frame.
Il sandboxing funziona sulla base di una lista consentita. Iniziamo rimuovendo tutte le autorizzazioni possibili, quindi riattiviamo le singole funzionalità aggiungendo flag specifici alla configurazione della sandbox. Per il widget di Twitter, abbiamo deciso di attivare JavaScript, i popup, l'invio di moduli e i cookie di twitter.com. Possiamo farlo aggiungendo un attributo sandbox a iframe con il seguente valore:
<iframe sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
src="https://platform.twitter.com/widgets/tweet_button.html"
style="border: 0; width:130px; height:20px;"></iframe>
È tutto. Abbiamo fornito al frame tutte le funzionalità di cui ha bisogno e il browser negherà l'accesso a tutti i privilegi che non abbiamo concesso esplicitamente tramite il valore dell'attributo sandbox.
Controllo granulare delle funzionalità
Nell'esempio precedente abbiamo visto alcuni dei possibili flag di sandboxing. Ora esaminiamo più nel dettaglio il funzionamento interno dell'attributo.
Dato un iframe con un attributo sandbox vuoto, il documento con frame verrà inserito completamente in una sandbox, sottoponendolo alle seguenti limitazioni:
- JavaScript non verrà eseguito nel documento con frame. Ciò include non solo JavaScript caricato esplicitamente tramite tag di script, ma anche gestori di eventi incorporati e URL javascript:. Significa anche che i contenuti contenuti nei tag noscript verranno visualizzati, esattamente come se l'utente avesse disattivato lo script.
- Il documento con frame viene caricato in un'origine univoca, il che significa che tutti i controlli della stessa origine non andranno a buon fine; le origini univoche non corrispondono mai ad altre origini, nemmeno a se stesse. Tra gli altri effetti, ciò significa che il documento non ha accesso ai dati memorizzati nei cookie di un'origine o in altri meccanismi di archiviazione (archiviazione DOM, IndexedDB e così via).
- Il documento con frame non può creare nuove finestre o finestre di dialogo (ad esempio tramite
window.openotarget="_blank"). - Impossibile inviare i moduli.
- I plug-in non verranno caricati.
- Il documento con frame può navigare solo al suo interno, non nel livello principale.
L'impostazione di
window.top.locationgenererà un'eccezione e il clic sul link contarget="_top"non avrà alcun effetto. - Le funzionalità che si attivano automaticamente (elementi dei moduli con messa a fuoco automatica, video con riproduzione automatica e così via) vengono bloccate.
- Impossibile ottenere il blocco del puntatore.
- L'attributo
seamlessviene ignorato suiframescontenuto nel documento con frame.
Si tratta di una misura piuttosto drastica e un documento caricato in un iframe completamente in sandbox
comporta un rischio davvero minimo. Naturalmente, non può nemmeno fare molto di utile: potresti riuscire a cavartela con una sandbox completa per alcuni contenuti statici, ma nella maggior parte dei casi vorrai allentare un po' le restrizioni.
Ad eccezione dei plug-in, ognuna di queste limitazioni può essere rimossa aggiungendo un flag al valore dell'attributo sandbox. I documenti in sandbox non possono mai eseguire plug-in, poiché i plug-in sono codice nativo non in sandbox, ma tutto il resto è consentito:
allow-formsconsente l'invio di moduli.allow-popupsconsente (sorpresa!) i popup.allow-pointer-lockconsente (sorpresa!) il blocco del puntatore.allow-same-originconsente al documento di mantenere la sua origine; le pagine caricate dahttps://example.com/manterranno l'accesso ai dati di quell'origine.allow-scriptsconsente l'esecuzione di JavaScript e l'attivazione automatica delle funzionalità (poiché l'implementazione tramite JavaScript sarebbe semplice).allow-top-navigationconsente al documento di uscire dal frame navigando nella finestra di primo livello.
Tenendo conto di ciò, possiamo valutare esattamente il motivo per cui abbiamo ottenuto l'insieme specifico di flag di sandboxing nell'esempio di Twitter riportato sopra:
allow-scriptsè obbligatorio, poiché la pagina caricata nel frame esegue un po' di JavaScript per gestire l'interazione dell'utente.allow-popupsè obbligatorio, poiché il pulsante apre un modulo per i tweet in una nuova finestra.allow-formsè obbligatorio, poiché il modulo per twittare deve essere inviabile.allow-same-originè necessario, altrimenti i cookie di twitter.com non sarebbero accessibili e l'utente non potrebbe accedere per pubblicare il modulo.
Una cosa importante da notare è che i flag di sandboxing applicati a un frame si applicano anche a tutte le finestre o i frame creati nella sandbox. Ciò significa che dobbiamo aggiungere allow-forms alla sandbox del frame, anche se il modulo esiste solo nella finestra che il frame apre.
Con l'attributo sandbox, il widget ottiene solo le autorizzazioni di cui ha bisogno e funzionalità come i plug-in, la navigazione di primo livello e il blocco del puntatore rimangono bloccate. Abbiamo ridotto il rischio di incorporare il widget, senza effetti negativi.
È un vantaggio per tutti i soggetti coinvolti.
Separazione dei privilegi
Il sandboxing dei contenuti di terze parti per eseguire il loro codice non attendibile in un ambiente con privilegi limitati è chiaramente vantaggioso. Ma che dire del tuo codice? Ti fidi di te, giusto? Quindi, perché preoccuparsi del sandboxing?
Io ribalterei la domanda: se il tuo codice non ha bisogno di plug-in, perché dargli accesso ai plug-in? Nella migliore delle ipotesi, si tratta di un privilegio che non utilizzi mai, nella peggiore è un potenziale vettore che gli utenti malintenzionati possono sfruttare per accedere. Il codice di tutti contiene bug e praticamente ogni applicazione è vulnerabile allo sfruttamento in un modo o nell'altro. La sandbox del tuo codice significa che anche se un utente malintenzionato riesce a compromettere la tua applicazione, non otterrà l'accesso completo all'origine dell'applicazione; potrà solo fare ciò che l'applicazione potrebbe fare. È comunque un problema, ma non così grave come potrebbe essere.
Puoi ridurre ulteriormente il rischio suddividendo l'applicazione in parti logiche e applicando il sandboxing a ciascuna parte con il minor numero di privilegi possibile. Questa tecnica è molto comune nel codice nativo: Chrome, ad esempio, si suddivide in un processo del browser con privilegi elevati che ha accesso al disco rigido locale e può stabilire connessioni di rete e in molti processi di rendering con privilegi ridotti che svolgono il lavoro più oneroso di analisi dei contenuti non attendibili. I renderer non devono toccare il disco, il browser si occupa di fornire loro tutte le informazioni necessarie per eseguire il rendering di una pagina. Anche se un hacker esperto trova un modo per danneggiare un renderer, non ha ottenuto molto, poiché il renderer non può fare molto di interessante da solo: tutti gli accessi con privilegi elevati devono essere indirizzati tramite il processo del browser. Gli utenti malintenzionati dovranno trovare diverse falle in diverse parti del sistema per causare danni, il che riduce notevolmente il rischio di pwnage riuscito.
Limitazione tramite sandbox sicura di eval()
Con il sandboxing e l'API postMessage, il successo di questo modello è abbastanza semplice da applicare al web. Parti della tua applicazione possono risiedere in iframe con sandbox e il documento principale può mediare la comunicazione tra di loro pubblicando messaggi e ascoltando le risposte. Questo tipo di struttura garantisce che gli exploit in qualsiasi parte dell'app causino il minor danno possibile. Inoltre, ha il vantaggio di costringerti a creare punti di integrazione chiari, in modo da sapere esattamente dove devi prestare attenzione alla convalida di input e output. Vediamo un esempio semplice per capire come potrebbe funzionare.
Evalbox è un'applicazione interessante che prende una stringa e la valuta come JavaScript. Incredibile, vero? Proprio quello che aspettavi da tutti questi anni. Si tratta di un'applicazione piuttosto pericolosa, ovviamente, poiché consentire l'esecuzione di JavaScript arbitrario significa che tutti i dati offerti da un'origine sono a disposizione. Mitigheremo il rischio che si verifichino eventi dannosi assicurandoci che il codice venga eseguito all'interno di una sandbox, il che lo rende molto più sicuro. Esamineremo il codice dall'interno verso l'esterno, iniziando dai contenuti del frame:
<!-- frame.html -->
<!DOCTYPE html>
<html>
<head>
<title>Evalbox's Frame</title>
<script>
window.addEventListener('message', function (e) {
var mainWindow = e.source;
var result = '';
try {
result = eval(e.data);
} catch (e) {
result = 'eval() threw an exception.';
}
mainWindow.postMessage(result, event.origin);
});
</script>
</head>
</html>
All'interno del frame, abbiamo un documento minimo che ascolta semplicemente i messaggi
dal suo elemento principale agganciandosi all'evento message dell'oggetto window.
Ogni volta che l'elemento principale esegue postMessage sui contenuti dell'iframe, questo evento
si attiva, consentendoci di accedere alla stringa che l'elemento principale vuole che
eseguiamo.
Nel gestore, prendiamo l'attributo source dell'evento, che è la finestra principale. Lo utilizzeremo per inviare il risultato del nostro duro lavoro una volta terminato. Poi faremo il grosso del lavoro, passando i dati che ci sono stati forniti a eval(). Questa chiamata è stata inserita in un blocco try, poiché le operazioni vietate all'interno di un iframe in sandbox generano spesso eccezioni DOM. Le intercettiamo e segnaliamo un messaggio di errore facile da comprendere. Infine, pubblichiamo il risultato nella finestra principale. Si tratta di un'operazione piuttosto semplice.
Il genitore è altrettanto semplice. Creeremo una piccola UI con un textarea per il codice e un button per l'esecuzione, quindi inseriremo frame.html tramite un iframe con sandbox, consentendo solo l'esecuzione dello script:
<textarea id='code'></textarea>
<button id='safe'>eval() in a sandboxed frame.</button>
<iframe sandbox='allow-scripts'
id='sandboxed'
src='frame.html'></iframe>
Ora collegheremo gli elementi per l'esecuzione. Innanzitutto, ascolteremo le risposte di iframe e le alert() ai nostri utenti. Presumibilmente un'applicazione reale farebbe qualcosa di meno fastidioso:
window.addEventListener('message',
function (e) {
// Sandboxed iframes which lack the 'allow-same-origin'
// header have "null" rather than a valid origin. This means you still
// have to be careful about accepting data via the messaging API you
// create. Check that source, and validate those inputs!
var frame = document.getElementById('sandboxed');
if (e.origin === "null" && e.source === frame.contentWindow)
alert('Result: ' + e.data);
});
Ora collegheremo un gestore di eventi ai clic su button. Quando l'utente fa clic, acquisiremo i contenuti correnti di textarea e li passeremo al frame per l'esecuzione:
function evaluate() {
var frame = document.getElementById('sandboxed');
var code = document.getElementById('code').value;
// Note that we're sending the message to "*", rather than some specific
// origin. Sandboxed iframes which lack the 'allow-same-origin' header
// don't have an origin which you can target: you'll have to send to any
// origin, which might alow some esoteric attacks. Validate your output!
frame.contentWindow.postMessage(code, '*');
}
document.getElementById('safe').addEventListener('click', evaluate);
Facile, no? Abbiamo creato un'API di valutazione molto semplice e possiamo essere certi che il codice valutato non abbia accesso a informazioni sensibili come cookie o spazio di archiviazione DOM. Allo stesso modo, il codice valutato non può caricare plug-in, aprire nuove finestre o svolgere altre attività fastidiose o dannose.
Puoi fare lo stesso per il tuo codice suddividendo le applicazioni monolitiche in componenti con un unico scopo. Ognuno può essere inserito in una semplice API di messaggistica, proprio come abbiamo scritto sopra. La finestra principale con privilegi elevati può fungere da controller e dispatcher, inviando messaggi a moduli specifici, ciascuno con il minor numero di privilegi possibile per svolgere il proprio lavoro, ascoltando i risultati e assicurandosi che ogni modulo riceva solo le informazioni necessarie.
Tuttavia, tieni presente che devi prestare molta attenzione quando gestisci contenuti con frame provenienti dalla stessa origine del livello principale. Se una pagina su https://example.com/ inserisce in un frame un'altra pagina della stessa origine con una sandbox che include i flag allow-same-origin e allow-scripts, la pagina inserita nel frame può raggiungere l'elemento principale e rimuovere completamente l'attributo sandbox.
Gioca nella tua sandbox
Il sandboxing è ora disponibile in diversi browser: Firefox 17 e versioni successive, IE10 e versioni successive e Chrome al momento della stesura di questo articolo (caniuse, ovviamente, ha una tabella di supporto aggiornata). L'applicazione dell'attributo sandbox
a iframes che includi ti consente di concedere determinati privilegi ai
contenuti che mostrano, solo i privilegi necessari per il
corretto funzionamento dei contenuti. In questo modo, hai l'opportunità di ridurre il rischio associato all'inclusione di contenuti di terze parti, oltre a quanto è già possibile con la Content Security Policy.
Inoltre, il sandboxing è una tecnica efficace per ridurre il rischio che un malintenzionato possa sfruttare le vulnerabilità del tuo codice. Separando un'applicazione monolitica in un insieme di servizi in sandbox, ognuno responsabile di una piccola parte di funzionalità autonome, gli utenti malintenzionati saranno costretti non solo a compromettere i contenuti di frame specifici, ma anche il loro controller. Si tratta di un'attività molto più difficile, soprattutto perché l'ambito del controller può essere notevolmente ridotto. Puoi dedicare i tuoi sforzi relativi alla sicurezza al controllo di quel codice se chiedi aiuto al browser per il resto.
Questo non significa che il sandboxing sia una soluzione completa al problema della sicurezza su internet. Offre una difesa in profondità e, a meno che tu non abbia il controllo sui client dei tuoi utenti, non puoi ancora fare affidamento sul supporto del browser per tutti i tuoi utenti (se controlli i client dei tuoi utenti, ad esempio un ambiente aziendale, evviva!). Un giorno… ma per ora il sandboxing è un altro livello di protezione per rafforzare le tue difese, non è una difesa completa su cui puoi fare affidamento in modo esclusivo. Tuttavia, i livelli sono eccellenti. Ti suggerisco di utilizzare questa opzione.
Per approfondire
"Privilege Separation in HTML5 Applications" è un articolo interessante che illustra la progettazione di un piccolo framework e la sua applicazione a tre app HTML5 esistenti.
La limitazione tramite sandbox può essere ancora più flessibile se combinata con altri due nuovi attributi iframe:
srcdoceseamless. Il primo consente di compilare un frame con contenuti senza l'overhead di una richiesta HTTP, mentre il secondo consente di applicare lo stile ai contenuti nel frame. Al momento, entrambi hanno un supporto del browser piuttosto scarso (Chrome e WebKit nightly), ma in futuro saranno una combinazione interessante. Ad esempio, puoi eseguire il sandboxing dei commenti su un articolo tramite il seguente codice:<iframe sandbox seamless srcdoc="<p>This is a user's comment! It can't execute script! Hooray for safety!</p>"></iframe>