Sicher in Sandbox-iFrames spielen

Damit die Nutzerfreundlichkeit des Internets von heute so einfach wie möglich ist, müssen Komponenten und Inhalte eingebettet werden, über die Sie keine wahre Kontrolle haben. Drittanbieter-Widgets können Interaktionen fördern und eine entscheidende Rolle für die allgemeine Nutzererfahrung spielen. Nutzergenerierte Inhalte sind manchmal sogar noch wichtiger als native Inhalte einer Website. Es ist keine Option, auf beides zu verzichten. Beide erhöhen jedoch das Risiko, dass etwas SchlechtesTM auf Ihrer Website passieren könnte. Jedes eingebettete Widget – jede Anzeige und jedes Social-Media-Widget – ist ein potenzieller Angriffsvektor für böswillige Nutzer:

Die Content Security Policy (CSP) kann die mit diesen beiden Inhaltstypen verbundenen Risiken mindern, da Sie die Möglichkeit haben, speziell vertrauenswürdige Skriptquellen und andere Inhalte auf die Zulassungsliste zu setzen. Das ist ein wichtiger Schritt in die richtige Richtung. Dennoch ist zu beachten, dass der Schutz, den die meisten CSP-Anweisungen bieten, binär ist: Die Ressource ist zugelassen oder nicht. Manchmal wäre es nützlich zu sagen: „Ich bin nicht sicher, ob ich dieser Inhaltsquelle vertraue, aber sie ist sooo hübsch! Bettet es ein, Browser, aber lass meine Website nicht kaputt.“

Prinzip der geringsten Berechtigung

Im Wesentlichen suchen wir nach einem Mechanismus, der es uns ermöglicht, eingebettete Inhalte nur in dem erforderlichen Umfang zur Verfügung zu stellen. Wenn für ein Widget kein neues Fenster erforderlich ist, kann es nicht passieren, dass der Zugriff auf window.open entfernt wird. Wenn Flash nicht erforderlich ist, sollte die Deaktivierung der Plug-in-Unterstützung kein Problem sein. Wir sind so sicher wie möglich, wenn wir dem Prinzip der geringsten Berechtigung folgen und jede einzelne Funktion blockieren, die für die gewünschten Funktionen nicht direkt relevant ist. Das hat zur Folge, dass wir nicht mehr blind darauf vertrauen müssen, dass ein Teil eingebetteter Inhalte keine Privilegien ausnutzen wird, die er nicht nutzen sollte. Es hat also einfach keinen Zugriff auf die Funktion.

iframe-Elemente sind der erste Schritt auf dem Weg zu einem guten Framework für eine solche Lösung. Wenn Sie einige nicht vertrauenswürdige Komponenten in einen iframe laden, wird eine gewisse Trennung zwischen Ihrer Anwendung und den zu ladenden Inhalten ermöglicht. Der Inhalt mit Frames hat keinen Zugriff auf das DOM Ihrer Seite oder lokal gespeicherte Daten. Außerdem kann der Inhalt nicht an beliebigen Positionen auf der Seite gezeichnet werden, da er auf den Umriss des Frames beschränkt ist. Die Trennung ist jedoch nicht wirklich zuverlässig. Auch auf der entsprechenden Seite gibt es noch eine Reihe von Optionen für störendes oder schädliches Verhalten. Automatische Wiedergabe von Videos, Plug-ins und Pop-ups sind die Spitze des Eisbergs.

Das sandbox-Attribut des Elements iframe liefert uns genau das, was wir brauchen, um die Einschränkungen für Inhalte in Frames zu verschärfen. Sie können den Browser anweisen, den Inhalt eines bestimmten Frames in einer Umgebung mit niedrigen Berechtigungen zu laden, sodass nur die Funktionen verfügbar sind, die für die Arbeit erforderlich sind.

Twust, aber verifizieren

Die Schaltfläche „Tweet“ von Twitter ist ein großartiges Beispiel für Funktionen, die du über eine Sandbox sicherer auf deiner Website einbetten kannst. Über Twitter können Sie mit folgendem Code 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, welche Funktionen gesperrt werden können, sehen wir uns genauer an, welche Funktionen die Schaltfläche benötigt. Der HTML-Code, der in den Frame geladen wird, führt JavaScript-Code von den Twitter-Servern aus und generiert ein Pop-up mit einer Tweeting-Oberfläche, wenn darauf geklickt wird. Diese Schnittstelle benötigt Zugriff auf die Cookies von Twitter, um den Tweet dem richtigen Konto zuzuordnen, und muss in der Lage sein, das Tweeting-Formular zu senden. Das ist schon recht viel. Der Frame muss keine Plug-ins laden, das Fenster auf oberster Ebene öffnen oder eine Reihe anderer Funktionen beinhalten. Da diese Berechtigungen nicht erforderlich sind, entfernen wir sie, indem wir den Inhalt des Frames in einer Sandbox ausführen.

Sandboxing basiert auf einer Zulassungsliste. Wir entfernen zuerst alle möglichen Berechtigungen und aktivieren dann einzelne Funktionen wieder, indem wir der Konfiguration der Sandbox bestimmte Flags hinzufügen. Für das Twitter-Widget haben wir uns entschieden, JavaScript, Pop-ups, Formularübermittlungen und 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 wars. Wir haben dem Frame alle erforderlichen Funktionen gegeben. Der Browser verweigert ihm dann den Zugriff auf Berechtigungen, die ihm nicht explizit über den Wert des Attributs sandbox gewährt wurden.

Detaillierte Kontrolle über Funktionen

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

Bei einem iFrame mit einem leeren Sandbox-Attribut wird das Dokument mit Frames vollständig in einer Sandbox ausgeführt. Dabei gelten die folgenden Einschränkungen:

  • JavaScript wird im Dokument mit Frames nicht ausgeführt. Dies umfasst nicht nur JavaScript, das explizit über Script-Tags geladen wird, sondern auch Inline-Event-Handler und JavaScript:-URLs. Inhalte in noscript-Tags werden also so angezeigt, als ob der Nutzer das Script selbst deaktiviert hätte.
  • Das geframete Dokument wird in einen eindeutigen Ursprung geladen. Das bedeutet, dass alle Prüfungen auf demselben Ursprung fehlschlagen. Eindeutige Ursprünge stimmen mit keinem anderen Ursprung überein, nicht einmal mit sich selbst. Dies bedeutet unter anderem, dass das Dokument keinen Zugriff auf Daten hat, die in den Cookies eines Ursprungs oder in anderen Speichermechanismen (DOM-Speicher, Indexed DB usw.) gespeichert sind.
  • Das gerahmte Dokument 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 Dokument kann nur in sich selbst navigieren, nicht in das übergeordnete Dokument auf oberster Ebene. Wenn Sie window.top.location festlegen, wird eine Ausnahme ausgelöst. Wenn Sie auf einen Link mit target="_top" klicken, hat dies keine Auswirkungen.
  • Automatisch ausgelöste Elemente (automatisch hervorgehobene Formularelemente, automatisch wiedergegebene Videos usw.) werden blockiert.
  • Zeigersperre kann nicht abgerufen werden.
  • Das Attribut seamless wird für iframes im gerahmten Dokument ignoriert.

Das sieht drakonisch aus und ein Dokument, das in eine vollständig in einer Sandbox ausgeführte iframe-Datei geladen wird, birgt tatsächlich nur ein sehr geringes Risiko. Natürlich ist auch nicht viel davon sinnvoll: Sie könnten vielleicht mit einer vollständigen Sandbox für einige statische Inhalte durchkommen, aber meistens sollten Sie die Dinge etwas lockern.

Mit Ausnahme von Plug-ins können jede dieser Einschränkungen aufgehoben werden, indem ein Flag zum Wert des Sandbox-Attributs 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 Fair Game:

  • Mit allow-forms können Formulare gesendet werden.
  • allow-popups erlaubt (schock!) Pop-ups.
  • allow-pointer-lock lässt (Überraschung!) Zeigersperre zu.
  • Mit allow-same-origin kann das Dokument seinen Ursprung beibehalten. Seiten, die von https://example.com/ geladen werden, haben weiterhin Zugriff auf die Daten dieses Ursprungs.
  • allow-scripts ermöglicht die Ausführung von JavaScript und ermöglicht, dass Funktionen automatisch ausgelöst werden (sie lassen sich einfach über JavaScript implementieren).
  • Mit allow-top-navigation kann das Dokument im Fenster auf oberster Ebene aus dem Frame herausgebrochen werden.

Vor diesem Hintergrund können wir genau auswerten, warum wir im Twitter-Beispiel oben die speziellen 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 eingereicht werden kann.
  • allow-same-origin ist erforderlich, da die Cookies von twitter.com ansonsten nicht zugänglich wären und sich der Nutzer nicht anmelden könnte, um das Formular zu posten.

Die auf einen Frame angewendeten Sandboxing-Flags gelten auch für alle in der Sandbox erstellten Fenster oder Frames. Das bedeutet, dass wir allow-forms zur Sandbox des Frames hinzufügen müssen, auch wenn das Formular nur in dem Fenster vorhanden ist, in dem der Frame angezeigt wird.

Mit dem Attribut sandbox erhält das Widget nur die erforderlichen Berechtigungen und Funktionen wie Plug-ins, Navigation oben und Zeigersperre bleiben blockiert. Wir haben das Risiko gesenkt, dass das Widget eingebettet ist, ohne Auswirkungen. Das ist ein Gewinn für alle Beteiligten.

Trennung von Berechtigungen

Die Sandbox-Technologie von Drittanbieterinhalten, um deren nicht vertrauenswürdigen Code in einer Umgebung mit niedrigen Berechtigungen auszuführen, ist selbstverständlich vorteilhaft. Aber was ist mit Ihrem eigenen Code? Du vertraust dir selbst, richtig? Warum sollten Sie sich also um die Sandboxing-Technologie kümmern?

Ich würde die Frage umdrehen: Warum sollte ich ihm Zugriff auf Plug-ins gewähren, wenn er keine Plug-ins benötigt? Im besten Fall ist dies ein Privileg, das Sie nie nutzen, im schlimmsten Fall ist es ein potenzielles Mittel für Angreifer, um einen Fuß in die Tür zu bekommen. Jeder Code hat Fehler und praktisch jede Anwendung ist anfällig für Sicherheitslücken. Das Sandboxing Ihres eigenen Codes bedeutet, dass ein Angreifer, selbst wenn er Ihre Anwendung erfolgreich manipuliert, keinen uneingeschränkten Zugriff auf den Ursprung der Anwendung erhält. Er kann nur Dinge tun, die die Anwendung könnte. Trotzdem 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 diese mit den minimalen Berechtigungen in einer Sandbox ausführen. Diese Technik ist bei nativem Code sehr üblich: Chrome bricht sich beispielsweise in einen Browserprozess mit hohen Berechtigungen ein, der Zugriff auf die lokale Festplatte hat und Netzwerkverbindungen herstellen kann, sowie viele Renderer-Prozesse mit niedrigen Berechtigungen, die das Parsen von nicht vertrauenswürdigen Inhalten schwierig ausführen. Renderer müssen die Festplatte nicht bearbeiten, der Browser gibt ihnen alle Informationen, die sie zum Rendern einer Seite benötigen. Selbst wenn ein raffinierter Hacker eine Möglichkeit findet, einen Renderer zu beschädigen, ist er noch nicht weit gekommen, da der Renderer alleine kaum von Interesse sein könnte: Der gesamte privilegierte Zugriff muss durch den Prozess des Browsers geleitet werden. Angreifer müssen in verschiedenen Teilen des Systems mehrere Löcher finden, um Schäden anzurichten, was das Risiko eines Pwnage-Angriffs erheblich reduziert.

Sicheres Sandboxing von eval()

Mit der Sandbox und der postMessage API ist der Erfolg dieses Modells recht einfach für das Web anwendbar. Teile Ihrer Anwendung können sich in iframes in einer Sandbox befinden und das übergeordnete Dokument kann die Kommunikation zwischen ihnen durchführen, indem es Nachrichten postet und auf Antworten wartet. Diese Art von Struktur sorgt dafür, dass Exploits in einem Teil der App so wenig wie möglich Schaden anrichten. Außerdem müssen Sie klare Integrationspunkte erstellen, damit Sie genau wissen, wo Sie bei der Validierung von Eingabe und Ausgabe sorgfältig vorgehen müssen. Schauen wir uns ein Beispiel mit einem Spielzeug an.

Evalbox ist eine spannende Anwendung, die einen String als JavaScript auswertet. Wow, oder? Genau das, worauf Sie so lange gewartet haben. Diese Anwendung ist natürlich eine ziemlich gefährliche Anwendung, da die Ausführung von beliebigem JavaScript dazu führt, dass alle Daten, die ein Ursprung zu bieten hat, zur Hand sind. Wir verringern das Risiko von „Bad ThingsTM“, indem wir dafür sorgen, dass der Code innerhalb einer Sandbox ausgeführt wird. Dadurch wird er wesentlich sicherer. Wir werden den Code von innen heraus durchgehen und mit dem Inhalt des Frames beginnen:

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

Der Frame enthält ein minimales Dokument, das einfach auf Nachrichten des übergeordneten Elements wartet, indem es in das message-Ereignis des window-Objekts einhängt. Immer wenn das übergeordnete Element postMessage für den Inhalt des iFrames ausführt, wird dieses Ereignis ausgelöst, wodurch wir Zugriff auf den String erhalten, den unser übergeordnetes Element ausführen soll.

Im Handler erfassen wir das Attribut source des Ereignisses, das dem übergeordneten Fenster entspricht. Damit senden wir das Ergebnis der harten Arbeit wieder, sobald sie fertig ist. Dann machen wir uns einen Großteil der Arbeit durch und übergeben die Daten, die wir erhalten haben, an eval(). Dieser Aufruf wurde in einen „try“-Block eingebunden, da gesperrte Vorgänge innerhalb einer Sandbox-iframe häufig DOM-Ausnahmen generieren. Diese werden von uns erkannt und stattdessen eine freundliche Fehlermeldung ausgegeben. Schließlich veröffentlichen wir das Ergebnis im übergeordneten Fenster. Das ist ziemlich einfach.

Das übergeordnete Element ist ebenfalls unkompliziert. Wir erstellen eine winzige UI mit einem textarea für Code und einem button für die Ausführung. Außerdem laden wir frame.html über eine iframe in der Sandbox, sodass nur das Skript ausgeführt werden kann:

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

Jetzt bereiten wir die Ausführung vor. Zuerst warten wir auf Antworten von iframe und alert() für unsere Nutzer. Vermutlich ist in einer echten Anwendung etwas weniger lästig:

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 verbinden wir einen Event-Handler für Klicks auf button. Wenn der Nutzer klickt, übernehmen wir den aktuellen Inhalt von textarea und übergeben ihn zur Ausführung in 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);

Einfach, oder? Wir haben eine sehr einfache Bewertungs-API erstellt, mit der wir sicher sein können, dass evaluierter Code keinen Zugriff auf vertrauliche Informationen wie Cookies oder DOM-Speicher hat. Außerdem kann evaluierter Code keine Plug-ins, keine neuen Fenster oder andere lästige oder schädliche Aktivitäten laden.

Sie können dasselbe mit Ihrem eigenen Code tun, indem Sie monolithische Anwendungen in Komponenten aufteilen, die einem einzigen Zweck dienen. Sie können wie oben beschrieben in einer einfachen Messaging-API zusammengefasst werden. Das übergeordnete Fenster mit hohen Berechtigungen kann als Controller und Dispatcher fungieren und Nachrichten an bestimmte Module senden, die jeweils die geringsten Rechte für ihre Arbeit haben. Es überwacht die Ergebnisse und sorgt dafür, dass jedes Modul nur mit den benötigten Informationen versorgt wird.

Bei Inhalten in Frames, die aus demselben Ursprung wie das übergeordnete Element stammen, müssen Sie jedoch sehr vorsichtig sein. Wenn eine Seite in https://example.com/ einen Frame für eine andere Seite desselben Ursprungs mit einer Sandbox aufweist, die sowohl das Flag allow-same-origin als auch das Flag allow-scripts enthält, kann die Seite mit Frames bis zur übergeordneten Seite gelangen und das Attribut „sandbox“ vollständig entfernen.

In der Sandbox spielen

Sandboxing ist jetzt in verschiedenen Browsern verfügbar: zum Zeitpunkt der Entstehung dieses Artikels in Firefox 17 und höher, Internet Explorer 10 und höher und Chrome (Caniuse hat natürlich eine aktuelle Supporttabelle). Wenn Sie das Attribut sandbox auf das Attribut iframes anwenden, können Sie für angezeigte Inhalte bestimmte Berechtigungen erteilen, nur die Berechtigungen, die für die korrekte Funktionsweise des Inhalts erforderlich sind. Auf diese Weise können Sie das mit der Aufnahme von Inhalten Dritter verbundene Risiko verringern, das weit über das hinausgeht, was mit der Content Security Policy bereits möglich ist.

Darüber hinaus ist die Sandbox-Technologie eine leistungsstarke Technik, um das Risiko zu verringern, dass ein raffinierter Angreifer in der Lage ist, Lücken in Ihrem eigenen Code auszunutzen. Durch die Unterteilung einer monolithischen Anwendung in eine Reihe von Diensten, die in einer Sandbox ausgeführt werden und von denen jeder für einen kleinen Teil eigenständiger Funktionalität verantwortlich ist, sind Angreifer nicht nur dazu gezwungen, den Inhalt bestimmter Frames, sondern auch deren Controller zu manipulieren. Das ist eine wesentlich schwierigere Aufgabe, insbesondere weil der Umfang des Controllers stark reduziert werden kann. Es ist sicherheitsrelevant, diesen Code zu prüfen, wenn Sie den Browser um Hilfe für den Rest bitten.

Das heißt aber nicht, dass die Sandbox-Technologie eine vollständige Lösung für das Sicherheitsproblem im Internet ist. Sie bietet gestaffelte Sicherheitsebenen. Sofern 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. Eines Tages... aber im Moment ist Sandboxing eine weitere Schutzebene zur Stärkung der Abwehr. Es ist keine vollständige Abwehr, auf die Sie sich allein verlassen können. Dennoch sind Layers einwandfrei. Ich schlage vor, dies zu nutzen.

Weiterführende Literatur

  • Der Artikel "Trennung von Berechtigungen in HTML5-Anwendungen" ist ein interessanter Artikel, der sich mit dem Design eines kleinen Frameworks und dessen Anwendung für drei bestehende HTML5-Anwendungen befasst.

  • Die Sandbox-Technologie kann in Kombination mit zwei weiteren neuen iFrame-Attributen noch flexibler sein: srcdoc und seamless. Mit dem Ersteren können Sie einen Frame mit Inhalt füllen, ohne dass eine HTTP-Anfrage erforderlich ist, und Letzteres ermöglicht das Einfließen von Stil in den Inhalt im Frame. Beide bieten im Moment eine ziemlich erbärmliche Browserunterstützung (Chrome und WebKit nächtlich), aber in Zukunft eine interessante Kombination sein. Sie können beispielsweise mit dem folgenden Code Kommentare zu einem Artikel in einer Sandbox ausführen:

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