Gioca in sicurezza negli iframe con sandbox

Creare un'esperienza ricca sul web odierno comporta quasi inevitabilmente l'incorporamento di componenti e contenuti sui quali non hai un reale controllo. I widget di terze parti possono aumentare il coinvolgimento e svolgere un ruolo fondamentale nell'esperienza utente complessiva; inoltre, i contenuti generati dagli utenti a volte sono ancora più importanti dei contenuti nativi di un sito. In realtà non è possibile astenersi dall'una o dall'altra, ma aumenta il rischio che si verifichi qualche problema sul sito. Ogni widget che incorpori, ogni annuncio, ogni widget di social media, è un potenziale vettore di attacco per gli utenti con intento malevolo:

Il Criterio di sicurezza del contenuto (CSP) può ridurre i rischi associati a entrambi questi tipi di contenuti offrendo la possibilità di inserire nella lista consentita origini di script e altri contenuti specificatamente affidabili. 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 non è consentita. A volte sarebbe utile dire "Non sono sicuro di fidarsi di questa fonte di contenuti, ma è davvero carino. Incorporalo per favore, Browser, ma non lasciare che comprometta 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 capacità necessario a svolgere il proprio lavoro. Se un widget non deve aprire una nuova finestra, rimuovere l'accesso a window.open non può essere dannoso. Se non richiede Flash, la disattivazione del supporto dei plug-in non dovrebbe essere un problema. Per garantire la massima sicurezza possibile, seguiamo il principio del privilegio minimo e blocchiamo tutte le funzionalità che non siano direttamente pertinenti alla funzionalità che desideriamo utilizzare. Il risultato è che non dobbiamo più confidare ciecamente che alcuni contenuti incorporati non sfruttino privilegi che non dovrebbero utilizzare. Semplicemente, non avrà accesso alla funzionalità.

Gli elementi iframe sono il primo passo verso la creazione di un framework efficace per una soluzione di questo tipo. Il caricamento di alcuni componenti non attendibili in un iframe fornisce una misura di separazione tra la tua applicazione e i contenuti che vuoi caricare. I contenuti con frame non avranno accesso al DOM della pagina o ai dati che hai archiviato localmente, né saranno in grado di tracciare posizioni arbitrarie nella pagina; hanno un ambito limitato rispetto alla struttura del frame. Tuttavia, la separazione non è realmente solida. La pagina contenuta presenta ancora una serie di opzioni per comportamenti fastidiosi o dannosi: la riproduzione automatica di video, plug-in e popup sono la punta dell'iceberg.

L'attributo sandbox dell'elemento iframe ci fornisce proprio ciò di cui abbiamo bisogno per intensificare le limitazioni sui contenuti con frame. Possiamo istruire il browser a caricare il contenuto di un frame specifico in un ambiente con privilegi limitati, consentendo solo il sottoinsieme di funzionalità necessarie per svolgere qualsiasi operazione.

Fai il conto, ma verifica

Il pulsante "Tweet" di Twitter è un ottimo esempio di funzionalità che può essere incorporata in modo più sicuro nel 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 che cosa possiamo bloccare, analizziamo attentamente le funzionalità richieste dal pulsante. Nel codice HTML caricato nel frame viene eseguita una porzione di codice JavaScript dai server di Twitter e, quando l'utente fa clic, viene generato un popup con un'interfaccia di tweeting. L'interfaccia deve poter accedere ai cookie di Twitter per collegare il tweet all'account corretto e poter inviare il modulo di tweet. Questo è tutto: il frame non ha bisogno di caricare plug-in, non deve navigare nella finestra di primo livello o in una serie di altre funzionalità. Poiché non ha bisogno di quei privilegi, li rimuoviamo 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 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à necessarie e il browser gli negherà utilmente l'accesso a privilegi che non gli abbiamo concesso esplicitamente tramite il valore dell'attributo sandbox.

Controllo granulare sulle capacità

Abbiamo visto alcuni dei possibili flag di sandbox nell'esempio sopra; ora esaminiamo un po' più in dettaglio il funzionamento interno dell'attributo.

Dato un iframe con un attributo sandbox vuoto, il documento racchiuso in frame verrà limitato completamente tramite sandbox, sottoposto alle seguenti restrizioni:

  • JavaScript non verrà eseguito nel documento racchiuso in frame. Ciò non include solo JavaScript caricato esplicitamente tramite tag di script, ma anche gestori di eventi incorporati e JavaScript: URL. Questo significa anche che i contenuti contenuti nei tag noscript verranno visualizzati, esattamente come se l'utente avesse disattivato lo script stesso.
  • 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 corrisponderanno mai ad altre origini, nemmeno se stesse. Tra le altre conseguenze, ciò significa che il documento non ha accesso ai dati archiviati nei cookie di qualsiasi origine o in altri meccanismi di archiviazione (archiviazione DOM, DB indicizzato e così via).
  • Il documento racchiuso in un frame non può creare nuove finestre o finestre di dialogo (tramite window.open o target="_blank", ad esempio).
  • Non è possibile inviare moduli.
  • I plug-in non verranno caricati.
  • La navigazione all'interno del documento racchiusa in un frame può essere solo se stessa, non l'elemento principale di primo livello. L'impostazione di window.top.location genererà un'eccezione e fare clic sul link con target="_top" non avrà alcun effetto.
  • Le funzionalità che si attivano automaticamente (elementi del modulo incentrati automaticamente, video con riproduzione automatica e così via) sono bloccate.
  • Impossibile ottenere il blocco del puntatore.
  • L'attributo seamless viene ignorato su iframes contenuto nel documento con frame.

Questo approccio è ben draconiano e un documento caricato in un iframe completamente sandbox comporta pochi rischi. Naturalmente non può rivelarsi molto utile: potresti riuscire a cavartela con una sandbox completa per alcuni contenuti statici, ma nella maggior parte dei casi dovrai fare in modo che le cose si dissolgano un po'.

Con l'eccezione dei plug-in, ognuna di queste limitazioni può essere rimossa aggiungendo un flag al valore dell'attributo sandbox. I documenti con sandbox non possono mai eseguire i plug-in, in quanto questi ultimi sono codice nativo senza sandbox, ma tutto il resto è considerato fair use:

  • allow-forms consente l'invio di moduli.
  • allow-popups consente i popup (stress!).
  • allow-pointer-lock consente il blocco del puntatore (sorpresa!).
  • allow-same-origin consente al documento di mantenere l'origine; le pagine caricate da https://example.com/ manterranno l'accesso ai dati di quell'origine.
  • allow-scripts consente l'esecuzione di JavaScript e l'attivazione automatica delle funzionalità (poiché sarebbe banale da implementare tramite JavaScript).
  • allow-top-navigation consente al documento di uscire dal frame navigando nella finestra di primo livello.

Tenendo a mente questi elementi, possiamo valutare esattamente perché abbiamo finito con l'insieme specifico di flag di sandbox nell'esempio di Twitter sopra:

  • È obbligatorio allow-scripts, poiché la pagina caricata nel frame esegue codice JavaScript per gestire l'interazione dell'utente.
  • È obbligatorio allow-popups, perché il pulsante apre un modulo di tweeting in una nuova finestra.
  • Il campo allow-forms è obbligatorio, perché il modulo di tweeting deve essere inviato.
  • allow-same-origin è necessario, perché i cookie di twitter.com non sarebbero altrimenti inaccessibili e l'utente non potrebbe accedere per pubblicare il modulo.

Una cosa importante da notare è che i flag della sandbox applicati a un frame vengono applicati 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 si apre.

Quando l'attributo sandbox è attivo, il widget riceve solo le autorizzazioni necessarie e funzionalità come plug-in, navigazione superiore e blocco del puntatore rimangono bloccate. Abbiamo ridotto il rischio di incorporare il widget, senza effetti negativi. È una conquista per tutti.

Separazione dei privilegi

La limitazione tramite sandbox di contenuti di terze parti per eseguire il proprio codice non attendibile in un ambiente con privilegi limitati è ovviamente vantaggioso. E per quanto riguarda il tuo codice? Ti fidi di te, vero? Perché preoccuparti dell'uso della sandbox?

Direi: se il vostro codice non ha bisogno di plug-in, perché permettergli di accedere a questi plug-in? Nella migliore delle ipotesi, è un privilegio che non usi mai, nella peggiore delle ipotesi è un potenziale vettore per entrare in ingresso. Tutti i codici hanno dei bug e praticamente ogni applicazione è vulnerabile allo sfruttamento in un modo o nell'altro. Con la sandbox del tuo codice si intende che, anche se un utente malintenzionato annullasse l'applicazione con successo, non gli verrà concesso l'accesso completo all'origine dell'applicazione; sarà solo in grado di eseguire le operazioni che potrebbe fare l'applicazione. Ancora male, ma non male come potrebbe essere.

Puoi ridurre ulteriormente il rischio suddividendo la tua applicazione in parti logiche e limitando la limitazione tramite sandbox di ogni parte con il privilegio minimo possibile. Questa tecnica è molto comune nel codice nativo: Chrome, ad esempio, si interrompe in un processo del browser con privilegi elevati che ha accesso al disco rigido locale e in grado di stabilire connessioni di rete, nonché molti processi di rendering con privilegi limitati che svolgono il lavoro pesante dell'analisi di contenuti non attendibili. I renderer non hanno bisogno di toccare il disco, ma il browser si occupa di fornire loro tutte le informazioni necessarie per la visualizzazione di una pagina. Anche se un hacker intelligente trova un modo per danneggiare un renderer, non è andato molto lontano, dato che quest'ultimo non può interessarsi da solo: tutti gli accessi con privilegi elevati devono essere indirizzati attraverso il processo del browser. Gli aggressori dovranno trovare diversi buchi in diversi punti del sistema per causare danni, il che riduce enormemente il rischio di peggio.

Limitazione tramite sandbox sicura eval()

Con il sandboxing e l'API postMessage, il successo di questo modello è abbastanza semplice da applicare al web. Parti della tua applicazione possono essere presenti in iframe con sandbox e il documento principale può mediare la comunicazione tra loro pubblicando messaggi e ascoltando le risposte. Questo tipo di struttura garantisce che gli exploit in qualsiasi parte dell'app causino il minimo danno possibile. Inoltre, presenta il vantaggio di dover creare punti di integrazione chiari, in modo da sapere esattamente dove devi prestare attenzione per convalidare input e output. Vediamo un esempio di giocattolo, solo per vedere come funziona.

Evalbox è un'applicazione interessante che prende una stringa e la valuta come JavaScript. Wow, vero? Proprio quello che stavi aspettando da tutti questi anni. È un'applicazione abbastanza pericolosa, ovviamente, in quanto consentire l'esecuzione arbitraria di JavaScript significa che tutti i dati che un'origine ha da offrire sono disponibili. Ridurremo il rischio che si verifichino Bad ThingsTM assicurando che il codice venga eseguito all'interno di una sandbox, rendendola un po' più sicura. Ci occuperemo del codice partendo dall'interno, 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, è presente un documento minimo che rimane in ascolto dei messaggi dell'oggetto principale collegandosi all'evento message dell'oggetto window. Ogni volta che il genitore esegue postMessage nei contenuti dell'iframe, viene attivato questo evento, che ci dà accesso alla stringa che il nostro genitore vuole che eseguiamo.

Nel gestore, prendiamo l'attributo source dell'evento, che è la finestra padre. Utilizzeremo questo indirizzo per inviare il risultato del nostro duro lavoro quando avremo finito. Faremo il resto, passando i dati che ci sono stati forniti in eval(). Questa chiamata è stata aggregata in un blocco "Prova", poiché le operazioni vietate all'interno di un iframe con sandbox generano spesso eccezioni DOM; le troveremo e segnaleremo un messaggio di errore descrittivo. Infine, pubblichiamo il risultato nella finestra principale. È roba piuttosto semplice.

Il padre è altrettanto semplice. Creeremo una piccola interfaccia utente con textarea per il codice e button per l'esecuzione, ed eseguiremo il pull di 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 inoltriamo l'ordine per l'esecuzione. Innanzitutto, ascolteremo le risposte di iframe e alert() dei nostri utenti. Presumiamo che 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" &amp;&amp; e.source === frame.contentWindow)
        alert('Result: ' + e.data);
    });

Successivamente, collegheremo un gestore di eventi ai clic su button. Quando l'utente fa clic, recuperiamo i contenuti attuali di textarea e li passiamo 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 quali cookie o spazio di archiviazione DOM. Allo stesso modo, il codice valutato non può caricare plug-in, popup di nuove finestre o altre attività fastidiose o dannose.

Puoi fare lo stesso per il tuo codice suddividendo le applicazioni monolitiche in componenti monouso. Ognuna può essere racchiusa in una semplice API di messaggistica, proprio come abbiamo scritto sopra. La finestra padre con privilegi elevati può fungere da controller e supervisore, inviando messaggi in moduli specifici che ognuno ha i privilegi minimi possibili per svolgere il proprio lavoro, ascoltando i risultati e garantendo che ogni modulo sia ben alimentato con solo le informazioni necessarie.

Tuttavia, tieni presente che devi fare molta attenzione quando hai a che fare con contenuti incorniciati che provengono dalla stessa origine dell'elemento principale. Se una pagina su https://example.com/ esegue il frame su un'altra pagina nella stessa origine con una sandbox che include entrambi i flag allow-same-origin e allow-scripts, la pagina con frame può arrivare all'elemento principale e rimuovere completamente l'attributo sandbox.

Gioca nella tua sandbox

Il sandboxing è ora disponibile in una serie di browser: Firefox 17 e versioni successive, IE10 e versioni successive e Chrome al momento della scrittura (caniuse, ovviamente, ha una tabella di supporto aggiornata). L'applicazione dell'attributo sandbox all'attributo iframes che includi ti consente di concedere determinati privilegi ai contenuti visualizzati, solo quelli necessari per il corretto funzionamento dei contenuti. Ciò ti offre l'opportunità di ridurre il rischio associato all'inclusione di contenuti di terze parti, andando oltre quanto già possibile con i Criteri di sicurezza del contenuto.

Inoltre, il sandboxing è una tecnica potente per ridurre il rischio che un aggressore intelligente possa sfruttare le lacune nel tuo codice. Separando un'applicazione monolitica in un insieme di servizi sandbox, ognuno responsabile di una piccola porzione di funzionalità autonome, gli utenti malintenzionati saranno costretti a compromettere non solo i contenuti di frame specifici, ma anche il controller. Questa è un'attività molto più difficile, soprattutto perché l'ambito del controller può essere ridotto. Puoi dedicare il tuo impegno relativo alla sicurezza a controllare quel codice se chiedi al browser di aiutarti con il resto.

Ciò non vuol dire che il sandboxing sia una soluzione completa al problema della sicurezza su internet. Offre una difesa approfondita e, se non hai il controllo sui client degli utenti, non puoi ancora fare affidamento sul supporto browser per tutti gli utenti (se controlli i client dei tuoi utenti, ad esempio un ambiente aziendale, urrà!). Un giorno... ma per ora la sandbox è un ulteriore livello di protezione che consente di rafforzare le difese, ma non è una difesa completa su cui puoi solo fare affidamento. I livelli sono comunque eccellenti. ti suggerisco di usarla.

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: srcdoc e seamless. La prima consente di completare un frame con contenuti senza l'overhead di una richiesta HTTP, mentre la seconda consente di inserire lo stile nei contenuti frame. Al momento entrambi hanno un supporto del browser abbastanza insoddisfacente (Chrome e WebKit notte), ma costituiranno una combinazione interessante in futuro. Ad esempio, puoi aggiungere i commenti tramite sandbox a un articolo utilizzando il seguente codice:

        <iframe sandbox seamless
                srcdoc="<p>This is a user's comment!
                           It can't execute script!
                           Hooray for safety!</p>"></iframe>