Sicher in Sandbox-iFrames spielen

Damit Sie im heutigen Web nutzerfreundlich sind, müssen Sie dabei fast unvermeidlich Komponenten und Inhalte einbetten, über die Sie keine Kontrolle haben. Widgets von Drittanbietern können die Interaktion fördern und eine wichtige Rolle für die Nutzerfreundlichkeit spielen. Von Nutzern erstellte Inhalte sind manchmal sogar wichtiger als die nativen Inhalte einer Website. Beides ist nicht wirklich eine Option, aber beides erhöht das Risiko, dass auf Ihrer Website etwas Schlimmes passiert. Jedes von Ihnen 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 mit beiden Arten von Inhalten verbundenen Risiken verringern, da Sie vertrauenswürdige Quellen von Scripts und anderen Inhalten auf die Zulassungsliste setzen können. Das ist ein wichtiger Schritt in die richtige Richtung. Der Schutz, den die meisten CSP-Richtlinien bieten, ist jedoch binär: Die Ressource ist entweder zulässig oder nicht. Manchmal ist es hilfreich, zu sagen: „Ich bin mir nicht sicher, ob ich dieser Inhaltsquelle wirklich vertraue, aber sie ist soooo schön! Bitte benutze den eingebetteten Code, Browser, aber lass meine Website nicht zusammenbrechen.“

Prinzip der geringsten Berechtigung

Im Wesentlichen suchen wir nach einem Mechanismus, mit dem wir Inhalte nur mit dem geringstmöglichen Funktionsumfang ausstatten können, der für ihre Aufgabe erforderlich ist. Wenn für ein Widget kein neues Fenster geöffnet werden 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 darstellen. Wir sind so sicher wie möglich, wenn wir das Prinzip der geringsten Berechtigung befolgen und jede einzelne Funktion sperren, die nicht direkt für die Funktionen, die wir verwenden möchten, relevant ist. So müssen wir nicht mehr blind darauf vertrauen, dass eingebettete Inhalte keine Berechtigungen nutzen, die sie nicht nutzen sollten. Es hat 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 einer iframe laden, wird Ihre Anwendung von den Inhalten getrennt, die Sie laden möchten. Der geframete Inhalt hat keinen Zugriff auf das DOM Ihrer Seite oder auf lokal gespeicherte Daten und kann auch nicht an beliebigen Stellen auf der Seite gezeichnet werden. Er ist auf den Umriss des Frames beschränkt. Die Trennung ist jedoch nicht wirklich robust. Die enthaltene Seite bietet jedoch weiterhin eine Reihe von Möglichkeiten für störendes oder schädliches Verhalten: automatisch abgespielte 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 eingebettete Inhalte zu verschärfen. Wir können den Browser anweisen, den Inhalt eines bestimmten Frames in einer Umgebung mit eingeschränkten Berechtigungen zu laden und nur die Funktionen zuzulassen, die für die jeweilige Aufgabe erforderlich sind.

Twust, aber überprüfen

Die Twitter-Schaltfläche „Tweet“ ist ein gutes Beispiel für Funktionen, die du über eine Sandbox sicherer auf deiner Website einbetten kannst. Mit dem folgenden Code kannst du die Schaltfläche über einen iframe einbetten:

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

Um herauszufinden, was wir sperren können, sehen wir uns genau an, welche Funktionen für die Schaltfläche erforderlich sind. Das in den Frame geladene HTML-Script führt ein wenig JavaScript von den Twitter-Servern aus und generiert beim Klicken ein Pop-up mit einer Tweet-Oberfläche. Diese Benutzeroberfläche 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 war es auch schon. Der Frame muss keine Plug-ins laden, das Fenster der obersten Ebene muss nicht geöffnet werden und es gibt keine weiteren Funktionen. Da diese Berechtigungen nicht benötigt werden, entfernen Sie sie, indem Sie den Inhalt des Frames in einer Sandbox ausführen.

Die Sandbox-Technologie basiert auf einer weißen Liste. Zuerst entfernen wir alle möglichen Berechtigungen und aktivieren dann einzelne Funktionen wieder, indem wir der Sandbox-Konfiguration bestimmte Flags hinzufügen. Für das Twitter-Widget haben wir uns entschieden, JavaScript, Pop-ups, Formulareinreichungen und Cookies von twitter.com zu aktivieren. Dazu fügen wir dem iframe das Attribut sandbox 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 gegeben und der Browser wird ihm den Zugriff auf alle Berechtigungen verweigern, die wir ihm nicht explizit über den Wert des Attributs sandbox gewährt haben.

Detaillierte Einstellungen für Funktionen

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

Wenn ein iFrame ein leeres Sandbox-Attribut hat, wird das geframete Dokument vollständig in die Sandbox verschoben und unterliegt den folgenden Einschränkungen:

  • JavaScript wird im Rahmen des Dokuments nicht ausgeführt. Dazu gehört nicht nur JavaScript, das explizit über Script-Tags geladen wird, sondern auch Inline-Ereignishandler und javascript:-URLs. Das bedeutet auch, dass Inhalte in noscript-Tags genau so angezeigt werden, als hätte der Nutzer das Script selbst deaktiviert.
  • Das geframete Dokument wird in einem eindeutigen Ursprung geladen. Das bedeutet, dass alle Prüfungen auf denselben Ursprung fehlschlagen. Eindeutige Ursprünge stimmen nie mit anderen Ursprüngen überein, auch nicht mit sich selbst. Das bedeutet unter anderem, dass das Dokument keinen Zugriff auf Daten hat, die in den Cookies eines Ursprungs oder in anderen Speichermechanismen (DOM-Speicher, Indexierte Datenbank usw.) gespeichert sind.
  • Über das geframete Dokument können keine neuen Fenster oder Dialogfelder geöffnet werden (z. B. über window.open oder target="_blank").
  • Formulare können nicht gesendet werden.
  • Plugins werden nicht geladen.
  • Das in einem Frame angezeigte Dokument kann nur sich selbst navigieren, nicht das übergeordnete Element der obersten Ebene. Wenn Sie window.top.location festlegen, wird eine Ausnahme ausgelöst und der Klick auf den Link mit target="_top" hat keine Auswirkungen.
  • Funktionen, die automatisch ausgelöst werden (z. B. automatisch fokussierte Formularelemente oder automatisch abgespielte Videos), werden blockiert.
  • Die Zeigersperre kann nicht abgerufen werden.
  • Das seamless-Attribut wird für iframes im geframeten Dokument ignoriert.

Das ist wirklich drakonisch und ein Dokument, das in eine vollständig in einer Sandbox ausgeführte iframe geladen wird, birgt tatsächlich ein sehr geringes Risiko. Natürlich bietet diese Methode auch keinen großen Nutzen: Sie können möglicherweise eine vollständige Sandbox für einige statische Inhalte nutzen, aber meistens wird es etwas lockerer sein.

Mit Ausnahme von Plug-ins kann jede dieser Einschränkungen aufgehoben werden, indem dem Wert des Sandbox-Attributs ein Flag hinzugefügt wird. In Sandbox-Dokumenten können keine Plug-ins ausgeführt werden, da es sich bei Plug-ins um nativen Code ohne Sandbox handelt. Alles andere ist jedoch erlaubt:

  • allow-forms ermöglicht das Senden von Formularen.
  • allow-popups erlaubt (oh Schreck!) Pop-ups.
  • Mit allow-pointer-lock können Sie den Cursor sperren.
  • Mit allow-same-origin kann der Ursprung des Dokuments beibehalten werden. Seiten, die über https://example.com/ geladen werden, behalten den Zugriff auf die Daten dieses Ursprungs.
  • allow-scripts ermöglicht die JavaScript-Ausführung und ermöglicht außerdem das automatische Auslösen von Funktionen, da die Implementierung über JavaScript einfach möglich wäre.
  • Mit allow-top-navigation kann das Dokument den Frame verlassen, indem im Fenster auf oberster Ebene navigiert wird.

Mit diesen Überlegungen können wir genau abschätzen, warum wir schließlich die spezifische Reihe von Sandboxing-Flags aus dem obigen Twitter-Beispiel erhalten haben:

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

Wichtig: Die Sandbox-Flags, die auf einen Frame angewendet werden, gelten auch für alle Fenster oder Frames, die in der Sandbox erstellt werden. Das bedeutet, dass wir allow-forms zur Sandbox des Frames hinzufügen müssen, auch wenn das Formular nur im Fenster vorhanden ist, in dem der Frame eingeblendet wird.

Wenn das Attribut sandbox vorhanden ist, erhält das Widget nur die erforderlichen Berechtigungen und Funktionen wie Plug-ins, obere Navigationsleiste und Zeigersperre bleiben blockiert. Wir haben das Risiko beim Einbetten des Widgets reduziert, ohne negative Auswirkungen. Das ist ein Gewinn für alle Beteiligten.

Trennung von Berechtigungen

Die Sandbox-Technologie für Drittanbieterinhalte, um nicht vertrauenswürdigen Code in einer Umgebung mit eingeschränkten Berechtigungen auszuführen, ist ziemlich offensichtlich von Vorteil. Aber was ist mit Ihrem eigenen Code? Sie vertrauen sich selbst, oder? Warum sollten Sie sich also Gedanken über die Sandbox machen?

Ich würde diese Frage umdrehen: Wenn Ihr Code keine Plug-ins benötigt, warum gewähren Sie ihm Zugriff darauf? Im besten Fall ist es ein Privileg, das Sie nie nutzen, im schlimmsten Fall ein potenzieller Angriffsvektor, mit dem Angreifer in Ihr System eindringen können. Der Code aller Entwickler enthält Fehler und praktisch jede Anwendung ist auf die eine oder andere Weise anfällig für Manipulationen. Wenn Sie Ihren eigenen Code in einer Sandbox platzieren, erhält ein Angreifer, selbst wenn er Ihre Anwendung erfolgreich manipuliert, keinen vollständigen Zugriff auf den Ursprung der Anwendung. Er kann nur Dinge tun, die auch die Anwendung tun könnte. Das ist zwar 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 minimalen Berechtigungen in einer Sandbox ausführen. Diese Technik ist bei nativem Code sehr verbreitet: Chrome setzt sich beispielsweise in einen Browserprozess mit hohen Berechtigungen ein, der Zugriff auf die lokale Festplatte hat und Netzwerkverbindungen herstellen kann. Außerdem gibt es viele Rendererprozesse mit niedrigen Berechtigungen, die das Parsen nicht vertrauenswürdiger Inhalte durchführen. Renderer müssen nicht auf die Festplatte zugreifen, da der Browser ihnen alle Informationen zum Rendern einer Seite zur Verfügung stellt. Selbst wenn ein cleverer Hacker einen Weg findet, einen Renderer zu manipulieren, ist er noch lange nicht am Ziel, da der Renderer allein nicht viel ausrichten kann: Der gesamte Zugriff mit erhöhten Berechtigungen muss über den Browserprozess geleitet werden. Angreifer müssen mehrere Sicherheitslücken in verschiedenen Teilen des Systems finden, um Schaden anzurichten. Das reduziert das Risiko eines erfolgreichen „Pwnage“ erheblich.

eval() sicher in einer Sandbox ausführen

Mit Sandboxing und der postMessage API lässt sich der Erfolg dieses Modells relativ einfach auf das Web übertragen. Teile Ihrer Anwendung können in Sandbox-iframes gespeichert werden. Das übergeordnete Dokument kann die Kommunikation zwischen ihnen vermitteln, indem es Nachrichten sendet und auf Antworten wartet. Diese Art von Struktur stellt sicher, dass Exploits in jedem Teil der App den minimalen Schaden verursachen. Außerdem zwingt es Sie dazu, klare Integrationspunkte zu erstellen, damit Sie genau wissen, wo Sie bei der Validierung von Eingabe und Ausgabe vorsichtig sein müssen. Sehen wir uns ein Spielzeugbeispiel an, um zu sehen, wie das funktionieren könnte.

Evalbox ist eine spannende Anwendung, die einen String als JavaScript auswertet. Wow, oder? Genau das, worauf Sie in all den langen Jahren gewartet haben. Das ist natürlich eine ziemlich gefährliche Anwendung, da die Ausführung beliebigen JavaScripts bedeutet, dass alle Daten eines Ursprungs infrage kommen. Wir minimieren das Risiko von „Schlimmen Dingen™“, indem wir dafür sorgen, dass der Code in einer Sandbox ausgeführt wird. Das macht ihn deutlich sicherer. Wir gehen den Code von innen nach außen durch, beginnend 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>

Innerhalb des Frames befindet sich ein minimales Dokument, das einfach auf Nachrichten von seinem übergeordneten Element wartet, indem es sich an das message-Ereignis des window-Objekts anschließt. Immer, wenn das übergeordnete Element postMessage für den Inhalt des iFrames ausführt, wird dieses Ereignis ausgelöst und gibt uns Zugriff auf den String, den das übergeordnete Element ausführen soll.

Im Handler greifen wir auf das source-Attribut des Ereignisses zu, also das übergeordnete Fenster. Über diese Adresse senden wir Ihnen das Ergebnis unserer Arbeit, sobald wir fertig sind. Dann übernehmen wir die schwere Arbeit und geben die angegebenen Daten an eval() weiter. Dieser Aufruf ist in einen „try“-Block eingebunden, da verbotene Vorgänge in einem iframe in einer Sandbox-Umgebung häufig DOM-Ausnahmen generieren. Diese werden abgefangen und stattdessen eine Fehlermeldung ausgegeben. Schließlich geben wir das Ergebnis an das übergeordnete Fenster zurück. Das ist ziemlich einfach.

Das übergeordnete Element ist ebenfalls unkompliziert. Wir erstellen eine kleine Benutzeroberfläche mit einem textarea für Code und einem button für die Ausführung. frame.html wird über eine sandboxierte iframe abgerufen, die nur die Scriptausführung zulässt:

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

Jetzt stellen wir die Ausführung zusammen. Zuerst hören wir uns die Antworten der iframe an und alert() sie an unsere Nutzer weiter. Eine echte Anwendung würde vermutlich etwas weniger Ärgerliches 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 hängen wir einen Event-Handler an Klicks auf die button an. Wenn der Nutzer klickt, erfassen wir den aktuellen Inhalt der textarea und übergeben ihn zur Ausführung an den Frame:

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 Bewertungs-API entwickelt und können sicher sein, dass der ausgewertete Code keinen Zugriff auf vertrauliche Informationen wie Cookies oder DOM-Speicher hat. Ausgewerteter Code kann auch keine Plug-ins laden, neue Fenster öffnen oder andere störende oder schädliche Aktivitäten ausführen.

Sie können das auch für Ihren eigenen Code tun, indem Sie monolithische Anwendungen in Komponenten mit nur einem Zweck unterteilen. Jedes Element kann in einer einfachen Messaging-API zusammengefasst werden, wie oben beschrieben. Das übergeordnete Fenster mit hohen Berechtigungen kann als Controller und Dispatcher fungieren, indem es Nachrichten an bestimmte Module sendet, die jeweils nur die geringstmöglichen Berechtigungen für ihre Aufgaben haben, auf Ergebnisse wartet und dafür sorgt, dass jedes Modul nur die erforderlichen Informationen erhält.

Beachten Sie jedoch, dass Sie sehr vorsichtig sein müssen, wenn Sie Inhalte in Frames verarbeiten, die denselben Ursprung wie der übergeordnete Inhalt haben. Wenn eine Seite auf https://example.com/ eine andere Seite mit demselben Ursprung in einem Frame mit einer Sandbox anzeigt, die sowohl die Flags allow-same-origin als auch allow-scripts enthält, kann die geframete Seite in die übergeordnete Seite eindringen und das Sandbox-Attribut vollständig entfernen.

In der Sandbox spielen

Sandboxing ist derzeit in einer Vielzahl von Browsern verfügbar: Firefox 17 und höher, IE 10 und höher sowie Chrome (caniuse hat natürlich eine aktuelle Supporttabelle). Wenn Sie das Attribut sandbox auf enthaltene iframes anwenden, können Sie für die angezeigten Inhalte bestimmte Berechtigungen erteilen, nur solche, die für das ordnungsgemäße Funktionieren des Inhalts erforderlich sind. So können Sie das Risiko, das mit der Einbindung von Drittanbieterinhalten verbunden ist, über das hinaus reduzieren, was mit der Content Security Policy bereits möglich ist.

Außerdem ist Sandboxing ein leistungsstarkes Verfahren, um das Risiko zu verringern, dass ein cleverer Angreifer Lücken in Ihrem eigenen Code ausnutzen kann. Wenn eine monolithische Anwendung in eine Reihe von Sandbox-Diensten aufgeteilt wird, die jeweils für einen kleinen Teil der eigenständigen Funktionen verantwortlich sind, müssen Angreifer nicht nur den Inhalt bestimmter Frames, sondern auch ihren Controller manipulieren. Das ist eine viel schwierigere Aufgabe, vor allem, da der Controller stark reduziert werden kann. Sie können Ihre Sicherheitsbemühungen auf die Prüfung dieses Codes konzentrieren, wenn Sie den Browser um Hilfe bei den restlichen Dingen bitten.

Das bedeutet nicht, dass die Sandbox-Technologie eine Komplettlösung für das Sicherheitsproblem im Internet ist. Es bietet eine mehrschichtige Verteidigung. Sofern Sie nicht die Kontrolle über die Clients Ihrer Nutzer haben, können Sie sich noch nicht auf die Browserunterstützung für alle Nutzer verlassen. Wenn Sie die Clients Ihrer Nutzer verwalten, z. B. in einer Unternehmensumgebung, ist das natürlich super. Irgendwann… aber im Moment ist Sandboxing eine weitere Schutzebene, um Ihre Abwehr zu stärken. Es ist keine vollständige Abwehr, auf die Sie sich allein verlassen können. Die Ebenen sind dennoch hervorragend. Ich empfehle Ihnen, diese zu verwenden.

Weiterführende Literatur

  • Privilege Separation in HTML5 Applications“ ist ein interessantes Dokument, in dem das Design eines kleinen Frameworks und seine Anwendung auf drei vorhandene HTML5-Apps beschrieben wird.

  • 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 ohne den Overhead einer HTTP-Anfrage mit Inhalten füllen. Mit letzterem kann der Stil in die geframeten Inhalte übernommen werden. Beide haben derzeit einen ziemlich miserablen Browsersupport (Chrome und WebKit-Nightlies), werden aber in Zukunft eine interessante Kombination sein. Mit dem folgenden Code können Sie beispielsweise Kommentare zu einem Artikel in der Sandbox testen:

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