Wenn Sie im heutigen Web eine ansprechende Website erstellen möchten, müssen Sie fast unvermeidlich Komponenten und Inhalte einbetten, über die Sie keine wirkliche 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 Grunde suchen wir nach einem Mechanismus, mit dem wir eingebetteten Inhalten nur die Mindestfunktionen gewähren können, die für ihre Funktion erforderlich sind. Wenn für ein Widget kein neues Fenster geöffnet werden muss, kann es nicht schaden, den Zugriff auf window.open zu entfernen. Wenn kein Flash erforderlich ist, sollte das Deaktivieren der Plug-in-Unterstützung kein Problem darstellen. Wir sind so sicher wie möglich, wenn wir dem Prinzip der geringsten Berechtigung folgen und jede Funktion blockieren, die nicht direkt für die von uns verwendeten Funktionen 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 Rahmen 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.
Vertrauen, aber prüfen
Die Schaltfläche „Tweeten“ von Twitter ist ein gutes Beispiel für eine Funktion, die über eine Sandbox sicherer in Ihre Website eingebettet werden kann. 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 dem richtigen Konto zuzuordnen, und muss das Formular zum Senden von Tweets 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 erforderlich sind, entfernen wir sie, indem wir den Inhalt des Frames in eine Sandbox verschieben.
Sandboxing funktioniert auf der Grundlage einer Zulassungsliste. 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 JavaScript, Pop-ups, die Formulareinreichung und die Cookies von twitter.com aktiviert. 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 zugewiesen. Der Browser verweigert ihm den Zugriff auf alle Berechtigungen, die wir ihm nicht explizit über den Wert des sandbox
-Attributs 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 geframeten Dokument 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 hat unter anderem zur Folge, 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
odertarget="_blank"
). - Formulare können nicht gesendet werden.
- Plugins werden nicht geladen.
- Das geframete Dokument kann nur selbst navigieren, nicht sein übergeordnetes Element auf oberster Ebene.
Wenn Sie
window.top.location
festlegen, wird eine Ausnahme ausgelöst und das Klicken auf den Link mittarget="_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üriframes
im geframeten Dokument ignoriert.
Das ist ziemlich drastisch und ein Dokument, das in eine vollständig sandboxierte iframe
geladen wird, stellt tatsächlich ein sehr geringes Risiko dar. Natürlich kann es auch nicht viel bringen: Für einige statische Inhalte reicht möglicherweise eine vollständige Sandbox aus, aber in den meisten Fällen sollten Sie die Regeln etwas 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 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 überhttps://example.com/
geladen werden, behalten den Zugriff auf die Daten dieses Ursprungs. allow-scripts
ermöglicht die Ausführung von JavaScript und die automatische Auslösung von Funktionen, da sie sich über JavaScript ganz einfach implementieren lassen.- Mit
allow-top-navigation
kann das Dokument den Frame verlassen, indem im Fenster auf oberster Ebene navigiert wird.
Anhand dieser Informationen können wir genau nachvollziehen, warum wir im obigen Twitter-Beispiel die spezifischen Sandbox-Flags verwendet haben:
allow-scripts
ist erforderlich, da auf der Seite, die in den Frame geladen wird, JavaScript für die Nutzerinteraktion ausgeführt wird.allow-popups
ist erforderlich, da die Schaltfläche ein Tweeting-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
der 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. Funktionen wie Plug-ins, die obere Navigationsleiste und die Maussperre bleiben blockiert. Wir haben das Risiko beim Einbetten des Widgets reduziert, ohne negative Auswirkungen.
Es ist eine Win-win-Situation 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 das Sandboxing machen?
Ich würde diese Frage umdrehen: Wenn Ihr Code keine Plug-ins benötigt, warum sollten Sie ihm Zugriff darauf gewähren? Im besten Fall ist es ein Privileg, das Sie nie nutzen, im schlimmsten Fall ein potenzieller Angriffsvektor, über den 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 ausführen, erhält ein Angreifer, selbst wenn er Ihre Anwendung erfolgreich manipuliert, keinen vollständigen Zugriff auf den Ursprung der Anwendung. Er kann nur Aktionen ausführen, die auch die Anwendung ausführen 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 jedes Teil mit den geringstmöglichen Berechtigungen in einer Sandbox platzieren. Diese Technik ist im Native Code sehr verbreitet: Chrome unterteilt sich beispielsweise in einen Browserprozess mit erhöhten Berechtigungen, der Zugriff auf die lokale Festplatte hat und Netzwerkverbindungen herstellen kann, und viele Renderer-Prozesse mit niedrigen Berechtigungen, die die schwere Arbeit beim Parsen nicht vertrauenswürdiger Inhalte übernehmen. 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 alleine nicht viel ausrichten kann: Der gesamte Zugriff mit erhöhten Berechtigungen muss über den Prozess des Browsers geleitet werden. Angreifer müssen mehrere Sicherheitslücken in verschiedenen Teilen des Systems finden, um Schaden anzurichten. Das Risiko eines erfolgreichen „Pwnage“ wird dadurch erheblich reduziert.
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-iframe
s gespeichert werden. Das übergeordnete Dokument kann die Kommunikation zwischen ihnen vermitteln, indem es Nachrichten sendet und auf Antworten wartet. Diese Art der Struktur sorgt dafür, dass Ausnutzungen in einem Teil der App möglichst wenig Schaden anrichten. 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 Beispiel an, um zu sehen, wie das funktionieren könnte.
Evalbox ist eine spannende Anwendung, die einen String als JavaScript auswertet. Wahnsinn, oder? Genau das, worauf Sie schon so lange 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.
Jedes Mal, wenn das übergeordnete Element postMessage für den Inhalt des iframe ausführt, wird dieses Ereignis ausgelöst. Dadurch erhalten wir Zugriff auf den String, den das übergeordnete Element ausführen möchte.
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 wurde in einen try-Block eingeschlossen, da verbotene Vorgänge in einer Sandbox-iframe
häufig DOM-Ausnahmen generieren. Wir fangen diese ab und melden stattdessen eine freundliche Fehlermeldung. 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. 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" && 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 einer Aufgabe unterteilen. Jede kann in eine einfache Messaging-API verpackt werden, genau 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.
Bei eingebetteten Inhalten, die aus derselben Quelle wie das übergeordnete Element stammen, ist jedoch Vorsicht geboten. 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 sandbox
-Attribut auf die von Ihnen eingefügte iframes
anwenden, können Sie den angezeigten Inhalten bestimmte Berechtigungen gewähren, nur die Berechtigungen, die für die korrekte Funktion der Inhalte 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 Sandboxing eine vollständige Lö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. Trotzdem sind Ebenen sehr nützlich. 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 weiteren neuen iframe-Attributen kombiniert wird:
srcdoc
undseamless
. 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>