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:

Content-Security-Policy: "default-src *; script-src example.org example2.org"

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:

Content-Security-Policy: script-src 'sha512-YWIzOWNiNzJjNDRlYzc4MTgwMDhmZDlkOWI0NTAyMjgyY2MyMWJlMWUyNjc1ODJlYWJhNjU5MGU4NmZmNGU3OAo='

Der benötigte Hash kann einfach mit gängigen CLI-Tools erzeugt werden:

echo -n "alert('Hello, world.');" | openssl dgst -sha256 -binary | openssl enc -base64

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:

{
  "csp-report": {
    "document-uri": "http://example.org/list",
    "referrer": "",
    "blocked-uri": "http://foo.bar/evilscript",
    "violated-directive": "script-src scripts.example.org",
    "original-policy": "default-src 'none'; script-src scripts.example.org; report-uri /csp/report"
  }
}

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:

default-src: 'self';

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:

script-src
  https://cm.g.doubleclick.net
  https://ssl.google-analytics.com
  'self'
  [..]
  'unsafe-eval'
  'nonce-ysLPrs5xcq+TOSHZeFjP6w=='
  https://*.twimg.com
  [..]
Schaut man sich auf der Seite um, sind alle Tags mit dem Nonce-Attribut versehen:
[..]
<script id="bouncer_terminate_iframe" nonce="ysLPrs5xcq+TOSHZeFjP6w==">
[..]

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:

frame-ancestors 'self';

Fonts dürfen ebenfalls nur von bestimmten Domains geladen werden oder müssen direkt über das “data”-Scheme geladen werden:

font-src
  [..]
  data:
  'self';

Bei Media wird das Schema “blob” erlaubt:

media-src
  [..]
  blob:
  'self';

Stylesheets dürfen auch wieder von einer Reihe CDN Domains geladen werden, können aber auch Inline auf der Seite benutzt werden:

style-src:
  [..]
  'unsafe-inline'
  'self';

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:

report-uri https://twitter.com/i/csp_report?a=NVQWGYLXFVZXO2LGOQ%3D%3D%3D%3D%3D%3D&ro=false;

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.