Content Security Policy – Tutorial

Das Problem Cross-Site-Scripting / XSS ist präsenter denn je – ständig hört man von neuen Angriffen mit teils verheerenden Folgen, die weit über das Entstellen von Gästebüchern hinausgehen. Seit 2009 in der Entwicklung und mittlerweile mit einer fast vollständigen Implementierung in Chrome und Firefox schickt sich die Content Security Policy nun an, XSS den Kampf anzusagen. Aktuell befindet sich die CSP noch im Status W3C Working Draft und wird speziell um HTML5-relevante Features wie etwa Web-Sockets ergänzt. Das heißt umgekehrt, dass man sie bereits heute problemlos verwenden kann. Aber von vorne.

Was ist die Content Security Policy

Haupt-Pattern bei XSS-Angriffen ist das Einschleusen von Inline-Script in den HTML-Code, der dann bei jedem Request mit ausgeliefert und ausgeführt wird. Javascript sollte in einer idealen Welt ohnehin ausschließlich in externen .js-Dateien ausgeliefert werden. So verfolgt die CSP das Konzept, Javascript nur dann auszuführen, wenn es sich in einem Script-File befindet. Selbstverständlich ist der Browser des Nutzers selbst für die Einhaltung der CSP zuständig, sodass der Schutz auf Clientseite erfolgt – Inline-Scripts werden also trotz verseuchtem HTML nicht ausgeführt, wenn der Browser des Nutzers CSP unterstützt. Hierzu müssen evtl. einige Anpassungen durchgeführt werden:

<a id="mylink" onclick="foo()">Foo</a>

… wird dann zu …

<script src="script.js"></script>
<a id="mylink">Foo</a>

… mit der Hilfe von jQuery in der Datei script.js:

$(document).ready(function()
{
    $("#mylink").on("click", function()
    {
        foo();
    });
});

Ohnehin eine gute Praxis, HTML und JS zu trennen.

Aber CSP kann noch mehr!

Die CSP beschränkt sich nicht auf die Bekämpfung von Inline-Scripten. Es können noch verschiedenste andere Ressourcen reglementiert werden, so etwa von welchen Locations Bilder und CSS-Dateien geladen werden dürfen. Denn auch von einem Angreifer eingeschleuste, externe CSS-Dateien und Bilder können eine Sicherheitsbedrohung darstellen. Die CSP ist so aufgebaut, dass sich für jede Regel Ausnahmen definieren lassen, aber dazu dann später mehr in den Beispielen.

Für einige Regeln wie z.B. die Behandlung von Javascript ist noch zusätzlich festgelegt, dass etwa eval nicht mehr ausgeführt wird. Dies kann aber auch wieder erlaubt werden. Weiterhin unterstützt die CSP ein Reporting-Verfahren: Bei jedem festgestellten Verstoß gegen die Policy wird ein vorher anzugebendes Script per POST-Request vom Browser des Nutzers aufgerufen und teilt dem Webmaster so mit, dass es potentielle XSS-Probleme auf der entsprechenden Webseite gibt. Auch toll: Es lässt sich ein Testmodus aktivieren, bei dem die CSP nicht aktiv durchgesetzt wird, wohl aber das Reporting angeschaltet ist – so lässt sich für den Webmaster schonmal grob abschätzen, was die Einführung der CSP auf seinen Seiten für Auswirkungen hätte.

Einbindung der CSP

Die CSP wird als HTTP-Header übermittelt. In einer früheren Version der Spezifikation wurde auch noch die Einbindung per Meta-Tag unterstützt, dies wurde aber mittlerweile wieder gestrichen. Der Header sieht im aktuellen, noch experimentierellen Status der Policy wie folgt aus:

Chrome

X-WebKit-CSP: <CSP-Regeln hier>

Internet-Explorer + Firefox

X-Content-Security-Policy: <CSP-Regeln hier>

Der finale Header nach Abschluss der Spezifikation

Content-Security-Policy: <CSP-Regeln hier>

Demnach empiehlt sich folgendes Script, um Redundanz zu vermeiden:

//sample rule
$csp_rules = "script-src 'self' cdnjs.cloudflare.com; style-src 'self'";

foreach (array("X-WebKit-CSP", "X-Content-Security-Policy", "Content-Security-Policy") as $csp)
{
	header($csp . ": " . $csp_rules);
}

Was umfasst die CSP alles?

  • script-src: Bestimmt, von welchen Domains externe Scripts geladen werden dürfen und ob Inline-Scripte erlaubt sind. Weiterhin kann hier eval wieder erlaubt werden, was standardmäßig durch die CSP verboten ist.
  • object-src: Besitmmt, von welchen Domains Flash und andere Plugins (Silverlight…) geladen werden – Gilt für die Tags <object>, <embed> und <applet>
  • style-src: Bestimmt, von welchen Domains CSS-Dateien geladen werden dürfen. Wichtig: Hier lässt sich nicht angeben, dass Inline-CSS supported werden soll. Das ist generell bei Verwendung der CSP ausgeschlossen.
  • img-src: Bestimmt, von welchen Domains Bilder geladen werden dürfen. Gilt nicht nur für den <img>-Tag sondern auch für CSS-Background-Images.
  • media-src: Bestimmt, von welchen Domains <video> und <audio>-Elemente geladen werden.
  • frame-src: Bestimmt, welche Seiten per Frame oder iframe eingebunden werden dürfen. frame-src https://facebook.com würde nur facebook-(i)frames durchgehen lassen.
  • font-src: Bestimmt, von welchen Domains externe Schriftarten mit der @font-face-Direktive geladen werden dürfen.
  • connect-src: Bestimmt, zu welchen Seiten eine Verbindung per WebSocket und XHR erlaubt wird.

Und ganz wichtig: default-src spezifiziert für alle Ressourcen einen Default-Wert, die nicht explizit aufgeführt wurden.

Standardverhalten

Standardmäßig verhält sich die CSP so, als wäre default-src: * eingestellt – heißt: Alle Ressourcen dürfen von überall geladen werden. Das ist erstmal nicht besonders sicher, unterbindet aber die Ausführung von Inline CSS, Inline Script und eval – wenn man dies nicht explizit erlaubt, was im Fall von Inline CSS garnicht möglich ist. Wie bereits erwähnt, wird durch default-src für alle Ressourcen ein Standard angegeben, die nicht gesondert in der CSP aufgeführt sind.

Konkrete Beispiele, bitte!

default-src 'self' cdn.foobar.de; script-src 'self' cdnjs.cloudflare.com; style-src 'self' static.ak.fbcdn.net
  • Alle Ressourcen dürfen standardmäßig von der eigenen Domain (’self‘) und von cdn.foobar.de geladen werden. Wichtig: ’self‘ steht in Hochkommas, weil es ein Keyword ist. Sonst würde self als Domain interpretiert werden. Dabei ist das Keyword ’self‘ streng mit der genauen Domain. Befindet sich eure Seite unter david.myspace.com und ’self‘ ist als Ressource gewhitelisted, darf nicht von other.myspace.com geladen werden. Umgekehrt: Wenn die Domain myspace.com ist und ’self‘ ist eingestellt, darf von script.myspace.com nicht geladen werden. Dies muss explizit noch mit angegeben werden.
  • Scripte dürfen nur von der eigenen Domain und cdnjs.cloudflare.com geladen werden. NICHT von cdn.foobar.de, denn default-src wird für Scripts überschrieben, sobald script-src explizit angegeben ist. Die Ausführung von Inline-Scripten ist nicht gestattet, eval wird auch nicht erlaubt.
  • Stylesheets dürfen von der eigenen Domain und von static.ak.fbcdn.net geladen werden. NIE sind Inline-Styles legitim.

Selbst ausprobieren?

<?php
$csp_rules = "default-src 'self' cdn.foobar.de; script-src 'self' cdnjs.cloudflare.com; style-src 'self' static.ak.fbcdn.net";

foreach (array("X-WebKit-CSP", "X-Content-Security-Policy", "Content-Security-Policy") as $csp)
{
	header($csp . ": " . $csp_rules);
}
?>

<link rel="stylesheet" href="http://static.ak.fbcdn.net/rsrc.php/v2/yW/r/54gK7YK85pd.css" />
<script src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.3.3/underscore-min.js"></script>
<script src="http://local/csp/s.js"></script>

<style>
h1 { color: red }
</style>

<h1>Rot?</h1>

<script>
alert("123")
</script>

Die Konsole im Chrome meldet sich wie folgt zu Wort:

CSP im Chrome

CSP im Chrome

Nächstes Beispiel

script-src 'self' 'unsafe-inline' 'unsafe-eval'

Hier wird die Verwendung der für script-src spezifischen Sonderregeln ‚unsafe-inline‘ und ‚unsafe-eval‘ gezeigt. Man beachte wieder die Hochkommas – schließlich sollen diese Schlüsselwörter nicht als Domain aufgefasst werden. Diese Regel ist im Übrigen nicht empfehlenswert, wird doch Code wie folgender dadurch ausgeführt – was die CSP quasi nutzlos macht:

<script>
alert(eval("2+3"))
</script>

‚unsafe-inline‘ und ‚unsafe-eval‘ müssen explizit „angeschaltet“ werden.

Nächstes Beispiel

default-src 'self' https://*.site.de; frame-src 'none'; object-src 'none'

Alle Inhalte werden von der eigenen Domain oder von einer beliebigen Subdomain von site.com geladen – allerdings nur per https. Frames und Object-Embeds sind nicht vorhanden und werden garnicht geladen. Das wars dann im Übrigen auch mit den Schlüsselwörtern.

Weitere Beispiele befinden sich drüben bei Mozilla.

Verwendung von Reporting

Wird an die CSP-Regel eine report-uri (absolut oder relativ) angehangen, bekommt diese per POST jeden Verstoß mitgeteilt und kann diesen dann weiterverarbeiten – Mailversand, Logdatei, Datenbank…

Ein Beispiel:

<?php
$csp_rules = "script-src 'self' 'unsafe-inline'; report-uri http://local/csp/reportcspviolation.php";

foreach (array("X-WebKit-CSP", "X-Content-Security-Policy", "Content-Security-Policy") as $csp)
{
	header($csp . ": " . $csp_rules);
}
?>
<script>
alert(eval("2+3"))
</script>

Da hier ‚unsafe-eval‘ nicht als Ausnahme angegeben ist, reported der Browser den Verstoß an die angegebene Report-URL.

CSP-Report

CSP-Report

Der Code der reportcspviolation.php-Datei könnte etwa wie folgt aussehen:

$c = file_get_contents("php://input");

if (!$c)
	exit;
	
$c = json_decode($c, true);
$c = print_r($c, true);

file_put_contents("csp.errors", $c, FILE_APPEND);

So wird uns schön mitgeteilt, auf welcher Seite gegen welche Direktive verstoßen wurde:

Array
(
    [csp-report] => Array
        (
            [document-uri] => http://local/csp/x.php
            [referrer] => 
            [blocked-uri] => self
            [violated-directive] => inline script base restriction
            [source-file] => http://local/csp/x.php
            [script-sample] => alert(eval("2+3"))
            [line-number] => 1
        )
)

Es ist auch möglich, nur das Reporting anzuschalten, die CSP selbst aber noch nicht durchzusetzen. Gut zum Experimentieren. So ist es auch denkbar, eine CSP-Policy im Einsatz zu haben, aber mit einer anderen zu experimentieren.

header("X-Content-Security-Policy: script-src 'self' 'unsafe-inline'; report-uri /activeviolation.php"
header("X-Content-Security-Policy-Report-Only: script-src 'self'; report-uri /evaluationviolation.php");

Hier lässt der Webmaster die obere CSP durchsetzen, experimentiert aber mit der unteren, strengeren CSP.

Empfehlungen und Bewertung

Noch ein paar warme Worte zum Schluss. Mit dem Firefox und Firebug hatte ich beim Experimentieren ein paar Probleme. Mag dran liegen, dass Firebug als Plugin nicht vollständig mit dem Browser verbandelt ist und so das ein oder andere nicht mitbekommt. Zumindest war die Konsole häufig unbrauchbar. Allerdings kann man sich beim Test ja mit der Reporting-URL behelfen, die alle Verstöße mitgeteilt bekommt.

Ein Tipp zum Aufbau der CSP-Regeln: Es wird empfohlen, erstmal alles zu verbieten und von da dann mit gezielten Ausnahmen die Policy wieder etwas zu „relaxxen“, wo nötig.

Das sieht dann etwa so aus:

X-Content-Security-Policy: default-src 'none'; script-src 'self' js.mysite.com; style-src 'self' css.mysite.com; img-src 'self' images.mysite.com

Ich denke, man versteht was gemeint ist. Hier wird durch die Direktive default-src erstmal per sé alles verboten und dann gezielte Ausnahmen für Scripts, CSS und Bilder ausgesprochen. Frames, Objects / Embeds, Audio etc. bleibt aber verboten, weil hier ja keine Ausnahme definiert wurde.

Ein Klasse Bookmarklet, welches eine Empfehlung für die CSP-Regeln basierend auf der aktuellen Seite ausspricht, sei auch noch empfohlen.

Die CSP befindet sich noch in der Entwicklung, deswegen lohnt ein Blick auf den W3C Working-Draft. Gerade wird etwa über script-nonce debattiert, um Inline-Scripte nur dann auszuführen, wenn sie ein nonce=“random_string“-Attribut mitbringen – So wird dem Browser mitgeteilt, dass das Inline-Script „intentional“ ist.

Weitere Posts:

Dieser Beitrag wurde unter php, Security, webdev veröffentlicht. Setze ein Lesezeichen auf den Permalink.

7 Antworten auf Content Security Policy – Tutorial

  1. Fabian sagt:

    Hi,

    ich hatte bei einem mittelgroßen Projekt CSPs produktiv im Einsatz.

    Dabei habe ich die Erfahrung gemacht, dass man mit CSP sehr schnell an Grenzen stößt und sich zeigt, dass die Spezifikation weit von der Realität entfernt ist.

    Folgende Probleme traten auf:
    – Nicht alle Eigenschaften wurden von Gecko oder Webkit unterstützt
    – Inline-Scripte per-se zu verbieten ist unrealistisch, weil man z.B. Konfigurationen benötigt oder JavaScript-Dateien asynchron laden möchte (Google Analytics) -> unsafe-inline werden alle die meisten Seiten benötigen (ohne nonce hat der Standard keinen Sinn)
    – eval() wird von vielen gepackten Scripten benötigt (Facebooks all.js) -> eval-script wird also auch benötigt
    – größtes Manko: Firefox frisst den Header nur bis zu einer bestimmten Länge. Wenn man eine einfache Webseite mit HTTP/HTTPS, Google Analytics, Facebook-Button und Googles Ajax Libraries betreibt dann braucht man schon folgende Ausnahmen:


    default-src 'self'
    http://*.google.com
    https://*.google.com
    http://*.google-analytics.com
    https://*.google-analytics.com
    http://ajax.googleapis.com/
    https://ajax.googleapis.com/
    http://*.facebook.net // ganz toll ;)
    https://*.facebook.net
    http://*.facebook.com
    https://*.facebook.com;
    img-src 'self'
    http://*.google.com
    https://*.google.com
    http://*.google-analytics.com
    https://*.google-analytics.com
    data:; // Facebook
    frame-src 'self'
    http://*.facebook.com
    https://*.facebook.com;
    options unsafe-inline inline-script eval-script;

    Kommen noch ein paar Ausnahmen dazu (Media-Server, CDNs) dann platzt die Liste und nix geht mehr ;) Warum im Standard nicht ein Dateiformat für CSP festgelegt wurde, erschließt sich mir nicht.

    Seit einigen Monaten sind daher CSPs ausgeschaltet. Leider.

  2. prof sagt:

    Hallo

    weißt du, wie ich mit der CSP undJquerys getScript umgehen muss?

    Ich habe zwei JS-Dateein die ich nur zum Login brauche, sie sind groß und ich will nicht, dass sie geladen werden wenn es nicht sein muss.

    Ich nutze in der getScript-Funktion den Error-Status und bei einem Fehler wird man auf eine Erklärseite umgeleitet. Ich habe mir nun, zu Testzwecken (Local auf XAMPP) die CSP eingebaut, es klappt soweit auch alles nur eben das getScript wirft Fehler bzw. lädt die benötigten Dateien nicht.

    Hast du nen Tipp?

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert