Sicher in Sandbox-iFrames spielen

Die Erstellung einer ansprechenden Website im heutigen Web erfordert fast zwangsläufig die Einbettung von Komponenten und Inhalten, über die Sie keine wirkliche Kontrolle haben. Widgets von Drittanbietern können die Interaktion fördern und eine entscheidende Rolle für die allgemeine Nutzererfahrung spielen. Von Nutzern erstellte Inhalte sind manchmal sogar wichtiger als die nativen Inhalte einer Website. Auf beides zu verzichten, ist keine wirkliche Option, aber beides erhöht das Risiko, dass auf Ihrer Website etwas Schlimmes passiert. Jedes eingebettete Widget – jede Anzeige, jedes Social-Media-Widget – ist ein potenzieller Angriffsvektor für Personen mit böswilligen Absichten:

Mit einer Content Security Policy (CSP) können Sie die Risiken im Zusammenhang mit beiden Arten von Inhalten minimieren, indem Sie vertrauenswürdige Quellen für Scripts und andere Inhalte auf die Zulassungsliste setzen. Das ist ein wichtiger Schritt in die richtige Richtung. Es ist jedoch zu beachten, dass der Schutz, den die meisten CSP-Anweisungen bieten, binär ist: Die Ressource ist entweder zulässig oder nicht. Manchmal wäre es nützlich, zu sagen: „Ich bin mir nicht sicher, ob ich dieser Inhaltsquelle wirklich vertraue, aber sie ist so schön! Bette es bitte ein, Browser, aber lass es nicht meine Website zerstören.“

Geringste Berechtigung

Im Grunde suchen wir nach einem Mechanismus, mit dem wir eingebetteten Inhalten nur die Mindestberechtigungen gewähren können, die für ihre Funktion erforderlich sind. Wenn ein Widget kein neues Fenster öffnen muss, kann es nicht schaden, den Zugriff auf „window.open“ zu entfernen. Wenn Flash nicht erforderlich ist, sollte das Deaktivieren der Plug-in-Unterstützung kein Problem sein. Wir sind so sicher wie möglich, wenn wir das Prinzip der geringsten Berechtigung befolgen und alle Funktionen blockieren, die nicht direkt für die von uns gewünschte Funktionalität relevant sind. Das Ergebnis ist, dass wir nicht mehr blind darauf vertrauen müssen, dass eingebettete Inhalte keine Berechtigungen nutzen, die sie nicht verwenden sollten. Sie haben dann einfach keinen Zugriff auf die Funktion.

iframe-Elemente sind der erste Schritt zu einem guten Framework für eine solche Lösung. Wenn Sie eine nicht vertrauenswürdige Komponente in einem iframe laden, wird Ihre Anwendung von den Inhalten, die Sie laden möchten, getrennt. Der gerahmte Inhalt hat keinen Zugriff auf das DOM Ihrer Seite oder auf lokal gespeicherte Daten. Außerdem kann er nicht an beliebigen Positionen auf der Seite dargestellt werden, sondern ist auf den Rahmen beschränkt. Die Trennung ist jedoch nicht wirklich robust. Die eingebettete Seite bietet immer noch eine Reihe von Optionen für störendes oder bösartiges Verhalten: Autoplay-Videos, Plug-ins und Pop-ups sind nur die Spitze des Eisbergs.

Das sandbox-Attribut des iframe-Elements bietet genau das, was wir brauchen, um die Einschränkungen für gerahmte Inhalte zu verschärfen. Wir können den Browser anweisen, den Inhalt eines bestimmten Frames in einer Umgebung mit geringen Berechtigungen zu laden, sodass nur die Teilmenge der Funktionen zulässig ist, die für die erforderlichen Aufgaben erforderlich sind.

Twust, but verify

Der „Tweet“-Button von Twitter ist ein gutes Beispiel für eine Funktion, die über eine Sandbox sicherer auf Ihrer Website eingebettet werden kann. Bei Twitter können Sie die Schaltfläche über einen iFrame einbetten. Verwenden Sie dazu den folgenden Code:

<iframe src="https://platform.twitter.com/widgets/tweet_button.html"
        style="border: 0; width:130px; height:20px;"></iframe>

Um herauszufinden, was wir einschränken können, sehen wir uns genau an, welche Funktionen die Schaltfläche benötigt. Der in den Frame geladene HTML-Code führt ein wenig JavaScript von den Twitter-Servern aus und generiert ein Pop-up, das beim Klicken mit einer Tweet-Oberfläche gefüllt wird. Diese Schnittstelle benötigt Zugriff auf die Cookies von Twitter, um den Tweet mit dem richtigen Konto zu verknüpfen, und muss das Tweet-Formular senden können. Das ist im Grunde alles. Der Frame muss keine Plug-ins laden, nicht im Fenster auf oberster Ebene navigieren und auch keine anderen Funktionen ausführen. Da diese Berechtigungen nicht erforderlich sind, entfernen wir sie, indem wir den Inhalt des Frames in einer Sandbox ausführen.

Sandboxing funktioniert auf Basis einer Zulassungsliste. Zuerst werden alle möglichen Berechtigungen entfernt. Anschließend werden einzelne Funktionen wieder aktiviert, indem der Konfiguration der Sandbox bestimmte Flags hinzugefügt werden. Für das Twitter-Widget haben wir uns entschieden, JavaScript, Pop-ups, das Senden von Formularen und die Cookies von twitter.com zu aktivieren. Dazu fügen wir der iframe ein sandbox-Attribut mit dem folgenden Wert hinzu:

<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>

Das war's. Wir haben dem Frame alle erforderlichen Funktionen zugewiesen und der Browser verweigert ihm den Zugriff auf alle Berechtigungen, die wir ihm nicht explizit über den Wert des Attributs sandbox gewährt haben.

Detaillierte Kontrolle über Funktionen

Im obigen Beispiel haben wir einige der möglichen Sandboxing-Flags gesehen. Sehen wir uns nun die Funktionsweise des Attributs etwas genauer an.

Wenn ein iFrame ein leeres Sandbox-Attribut hat, wird das Dokument im Frame vollständig in einer Sandbox ausgeführt und unterliegt den folgenden Einschränkungen:

  • JavaScript wird im gerahmten Dokument nicht ausgeführt. Dies umfasst nicht nur JavaScript, das explizit über Script-Tags geladen wird, sondern auch Inline-Event-Handler und javascript:-URLs. Das bedeutet auch, dass Inhalte in noscript-Tags so angezeigt werden, als hätte der Nutzer das Script selbst deaktiviert.
  • Das gerahmte Dokument wird in einen eindeutigen Ursprung geladen. Das bedeutet, dass alle Same-Origin-Prüfungen fehlschlagen. Eindeutige Ursprünge stimmen nie mit anderen Ursprüngen überein, nicht einmal mit sich selbst. Das bedeutet unter anderem, dass das Dokument keinen Zugriff auf Daten hat, die in Cookies oder anderen Speichermechanismen (DOM-Speicher, IndexedDB usw.) eines Ursprungs gespeichert sind.
  • Das Dokument im Frame kann keine neuen Fenster oder Dialogfelder erstellen (z. B. über window.open oder target="_blank").
  • Formulare können nicht gesendet werden.
  • Plug-ins werden nicht geladen.
  • Das gerahmte Dokument kann nur sich selbst, nicht aber das übergeordnete Element auf oberster Ebene steuern. Wenn Sie window.top.location festlegen, wird eine Ausnahme ausgelöst. Das Klicken auf einen Link mit target="_top" hat keine Auswirkungen.
  • Funktionen, die automatisch ausgelöst werden (Formularelemente mit Autofokus, automatisch wiedergegebene Videos usw.), werden blockiert.
  • Zeigersperre kann nicht abgerufen werden.
  • Das Attribut seamless wird für iframes im eingebetteten Dokument ignoriert.

Das ist eine sehr strenge Maßnahme und ein Dokument, das in eine vollständig in einer Sandbox ausgeführte iframe geladen wird, stellt ein sehr geringes Risiko dar. Natürlich kann sie auch nicht viel Sinnvolles tun: Für einige statische Inhalte ist eine vollständige Sandbox vielleicht ausreichend, aber meistens ist es besser, die Einschränkungen etwas zu lockern.

Mit Ausnahme von Plug-ins können alle diese Einschränkungen aufgehoben werden, indem dem Wert des Sandbox-Attributs ein Flag hinzugefügt wird. In einer Sandbox ausgeführte Dokumente können keine Plug-ins ausführen, da Plug-ins nativer Code ohne Sandbox sind. Alles andere ist jedoch möglich:

  • allow-forms ermöglicht das Senden von Formularen.
  • allow-popups erlaubt (Überraschung!) Pop-ups.
  • allow-pointer-lock ermöglicht (Überraschung!) die Zeigersperre.
  • Mit allow-same-origin behält das Dokument seinen Ursprung bei. Seiten, die von https://example.com/ geladen werden, behalten den Zugriff auf die Daten dieses Ursprungs.
  • allow-scripts ermöglicht die Ausführung von JavaScript und das automatische Auslösen von Funktionen, da diese über JavaScript einfach zu implementieren sind.
  • allow-top-navigation ermöglicht es dem Dokument, den Frame zu verlassen, indem es das übergeordnete Fenster aufruft.

Anhand dieser Kriterien können wir genau beurteilen, warum wir im Twitter-Beispiel oben die spezifischen Sandboxing-Flags verwendet haben:

  • allow-scripts ist erforderlich, da auf der in den Frame geladenen Seite JavaScript ausgeführt wird, um Nutzerinteraktionen zu verarbeiten.
  • allow-popups ist erforderlich, da die Schaltfläche ein Tweet-Formular in einem neuen Fenster öffnet.
  • allow-forms ist erforderlich, da das Tweet-Formular gesendet werden können muss.
  • allow-same-origin ist erforderlich, da sonst nicht auf die Cookies von twitter.com zugegriffen werden könnte und der Nutzer sich nicht anmelden könnte, um das Formular zu posten.

Wichtig ist, dass die auf einen Frame angewendeten Sandboxing-Flags auch für alle Fenster oder Frames gelten, die in der Sandbox erstellt werden. Das bedeutet, dass wir der Sandbox des Frames allow-forms hinzufügen müssen, obwohl das Formular nur in dem Fenster existiert, das der Frame öffnet.

Mit dem Attribut sandbox erhält das Widget nur die erforderlichen Berechtigungen. Funktionen wie Plug-ins, Top-Navigation und Zeigersperre bleiben blockiert. Wir haben das Risiko beim Einbetten des Widgets ohne negative Auswirkungen reduziert. Eine echte Win-win-Situation.

Rechtetrennung

Das Sandboxing von Inhalten von Dritten, um deren nicht vertrauenswürdigen Code in einer Umgebung mit geringen Berechtigungen auszuführen, ist offensichtlich von Vorteil. Aber was ist mit Ihrem eigenen Code? Du vertraust dir selbst, oder? Warum sollten Sie sich also Gedanken über Sandboxing machen?

Ich würde die Frage umdrehen: Wenn Ihr Code keine Plug-ins benötigt, warum sollte er dann Zugriff auf Plug-ins haben? Im besten Fall ist es eine Berechtigung, die Sie nie verwenden, im schlimmsten Fall ein potenzieller Angriffsvektor, über den Angreifer einen Fuß in die Tür bekommen können. Der Code aller Entwickler enthält Fehler und praktisch jede Anwendung ist auf die eine oder andere Weise anfällig für Angriffe. Wenn Sie Ihren eigenen Code in einer Sandbox ausführen, bedeutet das, dass ein Angreifer, selbst wenn er Ihre Anwendung erfolgreich unterwandert, keinen vollständigen Zugriff auf den Ursprung der Anwendung erhält. Er kann nur Dinge tun, die die Anwendung tun könnte. Das ist immer noch schlecht, aber nicht so schlimm, wie es sein könnte.

Sie können das Risiko noch weiter reduzieren, indem Sie Ihre Anwendung in logische Teile aufteilen und jeden Teil mit den geringstmöglichen Berechtigungen in einer Sandbox ausführen. Diese Technik ist in nativem Code sehr verbreitet: Chrome wird beispielsweise in einen Browserprozess mit hohen Berechtigungen, der Zugriff auf die lokale Festplatte hat und Netzwerkverbindungen herstellen kann, und viele Rendererprozesse mit niedrigen Berechtigungen aufgeteilt, die die aufwendige Analyse nicht vertrauenswürdiger Inhalte übernehmen. Renderer müssen nicht auf die Festplatte zugreifen. Der Browser stellt ihnen alle Informationen zur Verfügung, die sie zum Rendern einer Seite benötigen. Selbst wenn ein Hacker einen Weg findet, einen Renderer zu manipulieren, kommt er nicht weit, da der Renderer allein nicht viel ausrichten kann: Alle Zugriffe mit hohen Berechtigungen müssen über den Browserprozess geleitet werden. Angreifer müssen mehrere Sicherheitslücken in verschiedenen Teilen des Systems finden, um Schaden anzurichten. Dadurch wird das Risiko eines erfolgreichen Angriffs erheblich reduziert.

Sicheres Sandboxing von eval()

Mit Sandboxing und der postMessage API lässt sich der Erfolg dieses Modells ziemlich einfach auf das Web übertragen. Teile Ihrer Anwendung können in iframes in einer Sandbox ausgeführt werden. Das übergeordnete Dokument kann die Kommunikation zwischen ihnen vermitteln, indem es Nachrichten postet und auf Antworten wartet. Diese Art von Struktur sorgt dafür, dass Exploits in einem Teil der App so wenig Schaden wie möglich anrichten. Außerdem werden Sie so gezwungen, klare Integrationspunkte zu erstellen, sodass Sie genau wissen, wo Sie bei der Validierung von Ein- und Ausgaben vorsichtig sein müssen. Sehen wir uns ein einfaches Beispiel an, um zu sehen, wie das funktionieren könnte.

Evalbox ist eine interessante Anwendung, die einen String annimmt und als JavaScript auswertet. Wow, oder? Genau das, worauf Sie all die Jahre gewartet haben. Das ist natürlich eine ziemlich gefährliche Anwendung, da die Ausführung von beliebigem JavaScript bedeutet, dass alle Daten, die ein Ursprung zu bieten hat, abgegriffen werden können. Wir minimieren das Risiko, dass etwas Schlimmes passiert, indem wir dafür sorgen, dass der Code in einer Sandbox ausgeführt wird. Das macht ihn um einiges sicherer. Wir arbeiten uns von innen nach außen durch den Code und beginnen mit dem Inhalt des Frames:

<!-- 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>

Im Frame befindet sich ein minimales Dokument, das einfach auf Nachrichten von seinem übergeordneten Element wartet, indem es sich in das message-Ereignis des window-Objekts einklinkt. Wenn das übergeordnete Element „postMessage“ für den Inhalt des iFrames ausführt, wird dieses Ereignis ausgelöst. Dadurch erhalten wir Zugriff auf den String, den das übergeordnete Element ausführen möchte.

Im Handler rufen wir das Attribut source des Ereignisses ab, das das übergeordnete Fenster ist. Wir verwenden diese, um das Ergebnis unserer harten Arbeit zurückzusenden, wenn wir fertig sind. Dann erledigen wir die Hauptarbeit, indem wir die erhaltenen Daten an eval() übergeben. Dieser Aufruf wurde in einen try-Block eingeschlossen, da unzulässige Vorgänge in einem iframe-Sandbox-Element häufig DOM-Ausnahmen generieren. Wir fangen diese ab und geben stattdessen eine benutzerfreundliche Fehlermeldung aus. Schließlich wird das Ergebnis an das übergeordnete Fenster zurückgegeben. Das ist ziemlich einfach.

Das übergeordnete Element ist ähnlich unkompliziert. Wir erstellen eine kleine UI mit einem textarea für Code und einem button für die Ausführung. Außerdem binden wir frame.html über ein iframe in einer Sandbox ein, sodass nur die Skriptausführung möglich ist:

<textarea id='code'></textarea>
<button id='safe'>eval() in a sandboxed frame.</button>
<iframe sandbox='allow-scripts'
        id='sandboxed'
        src='frame.html'></iframe>

Jetzt verbinden wir die Elemente für die Ausführung. Zuerst warten wir auf Antworten von iframe und alert() sie an unsere Nutzer weiter. Eine echte Anwendung würde vermutlich etwas weniger Nerviges tun:

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);
    });

Als Nächstes verknüpfen wir einen Event-Handler mit Klicks auf das button. Wenn der Nutzer klickt, wird der aktuelle Inhalt des textarea abgerufen und zur Ausführung an den Frame übergeben:

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);

Ganz einfach, oder? Wir haben eine sehr einfache API für die Auswertung erstellt und können sicher sein, dass der ausgewertete Code keinen Zugriff auf vertrauliche Informationen wie Cookies oder DOM-Speicher hat. Ebenso kann ausgewerteter Code keine Plug-ins laden, keine neuen Fenster öffnen oder andere lästige oder schädliche Aktivitäten ausführen.

Sie können dasselbe für Ihren eigenen Code tun, indem Sie monolithische Anwendungen in Komponenten mit einem einzigen Zweck aufteilen. Jede kann in eine einfache Messaging-API eingebunden werden, wie oben beschrieben. Das übergeordnete Fenster mit hohen Berechtigungen kann als Controller und Dispatcher fungieren, Nachrichten an bestimmte Module senden, die jeweils die geringstmöglichen Berechtigungen für ihre Aufgaben haben, auf Ergebnisse warten und dafür sorgen, dass jedes Modul nur die erforderlichen Informationen erhält.

Beachten Sie jedoch, dass Sie beim Umgang mit gerahmten Inhalten, die vom selben Ursprung wie das übergeordnete Element stammen, sehr vorsichtig sein müssen. Wenn eine Seite auf https://example.com/ eine andere Seite desselben Ursprungs mit einer Sandbox einrahmt, die sowohl die Flags allow-same-origin als auch allow-scripts enthält, kann die eingerahmte Seite auf die übergeordnete Seite zugreifen und das Sandbox-Attribut vollständig entfernen.

In Ihrer Sandbox spielen

Sandboxing ist derzeit in verschiedenen Browsern verfügbar: Firefox 17+, IE10+ und Chrome (caniuse bietet eine aktuelle Support-Tabelle). Wenn Sie das Attribut sandbox auf die von Ihnen eingefügten iframes-Elemente anwenden, können Sie den angezeigten Inhalten bestimmte Berechtigungen gewähren, aber nur die Berechtigungen, die für die korrekte Funktion der Inhalte erforderlich sind. So können Sie das Risiko, das mit der Einbindung von Inhalten von Dritten verbunden ist, über das hinaus reduzieren, was bereits mit der Content Security Policy möglich ist.

Darüber hinaus ist Sandboxing eine leistungsstarke Technik, um das Risiko zu verringern, dass ein Angreifer Sicherheitslücken in Ihrem eigenen Code ausnutzen kann. Wenn Sie eine monolithische Anwendung in eine Reihe von Sandbox-Diensten aufteilen, die jeweils für einen kleinen Teil der eigenständigen Funktionalität verantwortlich sind, müssen Angreifer nicht nur den Inhalt bestimmter Frames, sondern auch deren Controller kompromittieren. Das ist eine viel schwierigere Aufgabe, insbesondere weil der Controller stark reduziert werden kann. Sie können Ihre sicherheitsrelevanten Bemühungen auf die Prüfung dieses Codes konzentrieren, wenn Sie den Browser um Hilfe beim Rest bitten.

Das bedeutet nicht, dass Sandboxing eine vollständige Lösung für das Problem der Sicherheit im Internet ist. Sie bietet eine gestaffelte Verteidigung. Wenn Sie keine Kontrolle über die Clients Ihrer Nutzer haben, können Sie sich noch nicht auf die Browserunterstützung für alle Ihre Nutzer verlassen. Wenn Sie die Clients Ihrer Nutzer kontrollieren, z. B. in einer Unternehmensumgebung, ist das natürlich kein Problem. Eines Tages vielleicht… Aber im Moment ist Sandboxing nur eine weitere Schutzebene, um Ihre Abwehr zu stärken, und keine vollständige Abwehr, auf die Sie sich allein verlassen können. Trotzdem sind Ebenen hervorragend. Ich empfehle, diese zu verwenden.

Weitere Informationen

  • Privilege Separation in HTML5 Applications ist ein interessantes Dokument, in dem das Design eines kleinen Frameworks und seine Anwendung auf drei bestehende HTML5-Anwendungen beschrieben werden.

  • Die Sandbox-Technologie kann noch flexibler sein, wenn sie mit zwei anderen neuen iframe-Attributen kombiniert wird: srcdoc und seamless. Mit Ersterem können Sie einen Frame mit Inhalten füllen, ohne dass eine HTTP-Anfrage erforderlich ist. Mit Letzterem können Sie den Stil in den gerahmten Inhalt einfließen lassen. Beide haben derzeit eine ziemlich schlechte Browserunterstützung (Chrome und WebKit Nightlies), werden aber in Zukunft eine interessante Kombination sein. Sie könnten beispielsweise Kommentare zu einem Artikel mit dem folgenden Code in eine Sandbox verschieben:

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