Cross-Site-Scripting (früher abgekürzt als CSS, inzwischen XSS) bezeichnet im Webumfeld eine Sicherheitslücke, die es dem Angreifer erlaubt, Schadcode im Browser des Nutzers auszuführen. Dabei läuft dieser Code direkt im Kontext der aufgerufenen Seite und nicht in irgend einer abgetrennten Sandbox. Verschiedene Angriffe sind seit Jahren bekannt, und Schutzmassnahmen wie die “same-origin”-Policy gibt es schon seit 1995. Über die Jahre sind Webanwendungen jedoch komplexer geworden und haben eine höhere Verbreitung erreicht. Damit bieten sie ein sehr attraktives Angriffsziel. Der Grundsatz, “Benutzereingaben validieren, Ausgaben encodieren”, ist in der Praxis natürlich nicht immer ganz so einfach umzusetzen.
Im Browser sind inzwischen weitere Schutzmassnahmen umgesetzt, unter anderem verschiedene “HTTP-Security-Header”. Dazu gehört auch der “Content Security Policy”-Standard. Die Idee dahinter ist, einfach festzulegen, welche Elemente auf einer Website aus welcher Quelle geladen werden dürfen. Schafft es ein Angreifer zum Beispiel JavaScript in die Seite zu injizieren, wird dies keinen Effekt haben, wenn durch die Policy vorher inline-JavaScript verboten wurde. Diese Maßnahmen sind allerdings nicht als Ersatz einer korrekten Validierung gedacht, sondern vielmehr als zusätzlicher Schutz. Diese helfen vor allem, wenn sich der Quellcode einer Anwendung aus unterschiedlichen Gründen nicht ändern lässt. Den passenden Content-Security-Policy Header kann man dann immer noch am Reverse-Proxy konfigurieren.
Die Policy wird von nahezu allen Browsern in der Version 1 (W3C Recommendation 2012) unterstützt. Die Version (“Level”) 2 in großen Teilen, außer von Microsofts IE/Edge. Im Januar diesen Jahres (2016) wurde der Entwurf für die nächste Version, Level 3, veröffentlicht.
Obwohl zum Beispiel
Twitter
und Disqus CSP-Header nutzen, ist die Verbreitung im Netz eher gering. Scannt
man zum Beispiel die Alexa Top 1.000.000 URL Liste, landet man mit ein paar
Fehlern bei knapp 859.202 Seiten. Davon setzen nur ungefähr 2.000 Seiten einen
Content-Security Header. Der Header macht natürlich bei vorwiegend statischen
Seiten weniger Sinn, allerdings wurde immerhin 42.000 mal der
X-XSS-Protection
-Header gesetzt.
Dieser Blogpost erklärt, wie man eine Content Security Policy einsetzen kann.
Header und Meta-Tags
Neben den inzwischen veralteten X-*-Headern lautet der korrekte Header-Name
Content-Security-Policy
. Eine Ausnahme bildet Microsofts IE 10/11. Hier wird
noch X-Content-Security-Policy
erwartet, Edge interpretiert inzwischen
allerdings den korrekten Header. Alternativ kann auch ein HTML Meta Tag benutzt
werden (http-equiv="Content-Security-Policy"
), allerdings gilt die CSP dann
natürlich erst ab dieser Stelle im Dokument und zum Beispiel nicht für
Prefetch-Requests. Werden mehrere CSPs angegeben, müssen alle Policies erfüllt
werden. Damit kann ein Angreifer, der in der Lage ist, zusätzliche Header zu
injizieren, trotzdem nicht die ursprüngliche Policy ausschalten.
Als Inhalt des Headers können dann die Policies, getrennt durch ein ;
,
angegeben werden. Jede Policy besteht aus einer Direktive und den dazu gehörigen
Werten, getrennt durch ein Leerzeichen.
Der folgende Header würde beispielsweise alle Inhalte erlauben, allerdings
JavaScript nur von den Hosts example.org
und example2.org
zulassen:
script-src Direktive
Die wichtigste Direktive ist script-src
. Sie bestimmt, aus welchen Quellen
JavaScript geladen und ausgeführt werden darf. Durch das Postfix “src” wird
deutlich gemacht, dass eine Liste aus vertrauenswürdigen Quellen erwartet wird.
Dabei erlaubt die Wildcard *
alleinstehend jede Quelle, kann aber auch mit
Hostnamen kombiniert werden. *.example.org foo.bar.com
würde zum Beispiel
alle Subdomains unter example.org
und explizit foo.bar.com
erlauben. Die
Angabe des Protokolls https:
ermöglicht es zudem, nur unter TLS gehostete
Ressourcen zu laden.
Das Setzen des Content-Security-Policy
-Headers verbietet weiterhin
inline-JavaScript und das Ausführen dynamischen Codes durch den Aufruf von
eval()
. Sollen diese Features auf bestimmten Ressourcen ermöglicht werden, zum
Beispiel für 3rd-Party Bibliotheken, können dazu in der Source-List die
Schlüsselwörter 'unsafe-inline'
und 'unsafe-eval'
benutzt werden.
'self'
whitelisted außerdem die eigene Domain, von der die aktuelle Seite
geladen wurde.
Nonces und Hashes
Wenn es nicht möglich ist, auf Inline-JavaScript komplett zu verzichten, gibt es
die Möglichkeit, gewollte Elemente mit einem bestimmten Wert, dem “nonce”, zu
versehen. Der Header muss dann als Script Source 'nonce-meinWert'
enthalten.
Die ausführbaren Scripte müssen dann mit dem Attribut nonce
und dem gleichen
Wert versehen werden. Dabei sollte dieser Wert bei jedem Request zufällig neu
generiert werden. Ein Angreifer kann nur dann Inline-Script Elemente verwenden,
wenn er irgendwie die Nonce erraten könnte.
Um sicherzustellen, dass der Inhalt einer Ressource nicht manipuliert wurde, gibt es noch die Möglichkeit, einen Hash zu verwenden. Als Algorithmus ist Beispielsweise SHA-512 verfügbar. Der Hash bezieht sich auf den Inhalt des Scripts und muss Base64-kodiert im Header angegeben werden, zum Beispiel:
Der benötigte Hash kann einfach mit gängigen CLI-Tools erzeugt werden:
Manipuliert ein Angreifer eine der nachgeladenen Ressourcen (zum Beispiel eine Bibliothek im CDN), wird diese Resource geblockt und vom Browser nicht weiter beachtet.
Weitere Direktiven
Neben script-src
gibt es noch eine ganze Reihe weiterer Direktiven, die sich
auf die unterschiedlichsten Medientypen beziehen. Dazu gehören zum Beispiel
style-src
, img-src
, font-src
, media-src
und weitere. Der Standardfall,
wenn kein expliziter Typ gesetzt ist, wird mit default-src
festgelegt. Durch
das Schlüsselwort 'none'
ist ausserdem die Möglichkeit gegeben, bestimmte
Medientypen gar nicht zu laden. Die Policy object-src 'none'
verhindert zum
Beispiel das Laden von <object>
, <embed>
und <applet>
Objekten.
Zielhosts für XMLHttpRequest
, WebSocket
oder EventSource
können mittels
connect-src
eingeschränkt werden. Um Sandboxing für bestimmte Ressourcen zu
aktivieren, d.h. die “Same-Origin-Policy” durchzusetzen, Popups und Scripts zu
verbieten, kann man sandbox
nutzen. allow-forms
würde die Verwendung von
Formularen erlauben, aber JavaScript weiterhin nicht zulassen.
<object>
und <embed>
können völlig unterschiedlichen Content laden. Dem
wird seit Level 2 mit der plugin-types
Direktive Rechnung getragen. Dadurch
lassen sich die MIME-types zum Beispiel auf “application/x-java-applet” oder
“application/pdf” einschränken.
Im Rahmen von Level 2 wurde der Standard um eine Reihe weiterer Direktiven
erweitert. Dabei wurde auf Web Worker Bezug genommen (child-src
), die
benutzten Quellen für Formular-Actions können mit form-action
definiert werden
und eine engere Kontrolle zum Einbinden der Seite durch <frame>
,
<iframe>
-Elemente kann durch frame-ancestors
erreicht werden.
Reporting
Content Security Policy-Verstöße können vom Browser an eine bestimmte URI
weitergeleitet werden. Diese URI wird mit der Direktive report-uri
gesetzt.
Bei einem Verstoß sendet der Browser einen Post-Request mit zum Beispiel
folgender JSON-Payload:
Enthalten ist neben der aufgerufenen URI und dem Refer(r)er die URI des geblockten Requests. Zur Analyse findet sich dabei noch die vollständige Policy und explizit die verletzte Direktive. Der Report wird dabei asynchron zum eigentlichen Ladevorgang der Seite aufgerufen und blockiert damit nicht.
Reporting ist aber nicht nur zur Kontrolle der aktiven Policies und zum Auffinden
potentieller XSS-Lücken interessant. Wird anstatt des normalen CSP-Headers der
Header Content-Security-Policy-Report-Only
benutzt, werden Verstöße nicht
geblockt, aber trotzdem gemeldet. Der Header kann auch zusätzlich zu den
bestehenden Policies benutzt werden, um zukünftige Änderungen gefahrlos im
Betrieb zu testen.
CSP in der Praxis bei Twitter
Twitter nutzt seine CSP hauptsächlich, um die Quellen für verschiedene Medientypen einzuschränken, und kann uns hier als Beispiel dienen, wie die eingesetzten Direktiven in der Praxis verwendet werden.
Zur Vollständigkeit hier der gesamte Header:
content-security-policy: script-src https://connect.facebook.net https://cm.g.doubleclick.net https://ssl.google-analytics.com https://graph.facebook.com https://twitter.com 'unsafe-eval' https://\*.twimg.com https://api.twitter.com https://analytics.twitter.com https://publish.twitter.com https://ton.twitter.com https://syndication.twitter.com https://www.google.com https://t.tellapart.com https://platform.twitter.com 'nonce-RKwXnvlQhgxVTvmUGuSZcw==' https://www.google-analytics.com 'self'; frame-ancestors 'self'; font-src https://twitter.com https://\*.twimg.com data: https://ton.twitter.com https://fonts.gstatic.com https://maxcdn.bootstrapcdn.com https://netdna.bootstrapcdn.com 'self'; media-src https://twitter.com https://\*.twimg.com https://ton.twitter.com blob: 'self'; connect-src https://graph.facebook.com https://\*.giphy.com https://\*.twimg.com https://api.twitter.com https://pay.twitter.com https://analytics.twitter.com https://media.riffsy.com https://embed.periscope.tv https://upload.twitter.com https://api.mapbox.com 'self'; style-src https://fonts.googleapis.com https://twitter.com https://\*.twimg.com https://translate.googleapis.com https://ton.twitter.com 'unsafe-inline' https://platform.twitter.com https://maxcdn.bootstrapcdn.com https://netdna.bootstrapcdn.com 'self'; object-src https://twitter.com https://pbs.twimg.com; default-src 'self'; frame-src https://staticxx.facebook.com https://twitter.com https://\*.twimg.com https://5415703.fls.doubleclick.net https://player.vimeo.com https://pay.twitter.com https://www.facebook.com https://ton.twitter.com https://syndication.twitter.com https://vine.co twitter: https://www.youtube.com https://platform.twitter.com https://upload.twitter.com https://s-static.ak.facebook.com 'self' https://donate.twitter.com; img-src https://graph.facebook.com https://\*.giphy.com https://twitter.com https://\*.twimg.com data: https://lumiere-a.akamaihd.net https://fbcdn-profile-a.akamaihd.net https://www.facebook.com https://ton.twitter.com https://\*.fbcdn.net https://syndication.twitter.com https://media.riffsy.com https://www.google.com https://stats.g.doubleclick.net https://\*.tiles.mapbox.com https://www.google-analytics.com blob: 'self'; report-uri https://twitter.com/i/csp_report?a=NVQWGYLVVVZXO2LGOQ%3D%3D%3D%3D%3D%3D&ro=false;
Dividiert man diesen Header etwas auseinander, findet man als Default-Quelle erstmal nur die eigene Seite:
Als Quelle für JavaScript wird nicht nur die eigene URI, unter der die Seite ausgeliefert wurde, angegeben, sondern noch eine ganze Reihe an eigenen Subdomains und CDN Hosts. Inline JavaScript wird erlaubt, allerdings muss jedes Script Tag mit einer Nonce versehen sein:
Schaut man sich auf der Seite um, sind alle Tags mit dem Nonce-Attribut versehen:Ruft man die Seite erneut auf, wechselt die Nonce.
Das Einbinden der Seite in Frames unter einer anderen Domain wird, unabhängig von der gesetzten CSP, durch das Setzen von “x-frame-options:SAMEORIGIN” verhindert. Trotzdem wird dies auch nochmal in der CSP angegeben:
Fonts dürfen ebenfalls nur von bestimmten Domains geladen werden oder müssen direkt über das “data”-Scheme geladen werden:
Bei Media wird das Schema “blob” erlaubt:
Stylesheets dürfen auch wieder von einer Reihe CDN Domains geladen werden, können aber auch Inline auf der Seite benutzt werden:
Darüber hinaus sind noch eine Reihe an weiteren Direktiven wie frame-src, img-src, connect-src oder object-src im Einsatz.
Die Report-URI enthält offenbar noch zusätzliche Informationen, encoded in einem Identifier:
Weitere Informationen
Einen ausführlichen Überblick zur CSP findet man beim Mozilla Developer Network. Dort sind auch nochmal alle Direktiven ausführlich als Referenz zusammengefasst.
Wer noch tiefere Dokumentation sucht, dem sei die Canidate Recommendation zu CSP 1.0 oder Level 2 empfohlen. Eine aktuelle Arbeitskopie der Level 3 Spezifikation findet man im W3C Repository auf Github: https://github.com/w3c/webappsec-csp.
Informationen zur Browser-Kompatibilität der einzelnen Direktiven gibt es auf Content-Security-Policy.com.