Was ist eine Content-Security-Policy (CSP)
Eine CSP ist eine Ansammlung von Direktiven und Anweisungen, die definieren, wie ein Browser sich – gerade im Bezug auf das Laden von Skripten und Stylesheets – zu verhalten hat.
Dies ist insbesondere im Hinblick auf XSS-Angriffe auf die eigene Webseite von grosser Bedeutung, da bösartige Akteure versuchen könnten, Code auf die Seite einzuschleusen und somit nichts ahnende Benutzer auf Phishing-Webseiten weiterzuleiten oder deren Cookies abzugreifen.
Mit einer CSP kann man:
- angeben, welche JavaScript und andere Dateien geladen werden dürfen, und so das Laden nicht befugten Codes unterbinden
- inline
<script>
-Elemente unterbinden - nur
<script>
-Elemente erlauben, welche über den korrekten Hash oder Nonce verfügen - inline event handler unterbinden
javascript:
URL’s unterbinden- gefährliche API’s wie
eval()
unterbinden
Dieser Beitrag soll einen groben Überblick über die Thematik verschaffen und kann als Startpunkt für die eigene CSP dienen.
Grundlegendes
Es gibt im Grunde nur zwei Möglichkeiten, eine Content-Security-Policy zu implementieren.
- Die erste Möglichkeit ist, das HTML
<meta>
-Element zu verwenden, wobei hier gesagt werden muss, dass diese Variante gewisse Einschränkungen mit sich bringt, aufgrund dessen diese Variante nicht näher beleuchtet wird. - Der zweite Ansatz baut auf dem HTTP-Response-Header Content-Security-Policy auf.
Die Policy muss zwingend in jedem Aufruf vorhanden sein und nicht nur auf der Hauptseite.
Die Richtlinie wird als eine Reihe von Direktiven angegeben, die durch Semikolons voneinander getrennt sind. Jede Direktive steuert einen anderen Aspekt der Sicherheitsrichtlinie. Jede Direktive hat einen Namen, gefolgt von einem Leerzeichen und einem Wert. Verschiedene Direktiven können unterschiedliche Syntaxen haben.
Beispiel CSP Anhand eines Response-Headers:
Content-Security-Policy: default-src 'self'; img-src 'self' example.com
Die erste Direktive, default-src, weist den Browser an, nur Ressourcen – wie JavaScript oder CSS – zu laden, die denselben Ursprung wie das Dokument haben, es sei denn, andere spezifischere Anweisungen legen eine andere Richtlinie für andere Ressourcentypen fest. Die zweite Anweisung, img-src, weist den Browser an, Bilder zu laden, die denselben Ursprung haben oder von „example.com“ bereitgestellt werden.
Je nach Art der Ressource gibt es unterschiedliche Direktiven:
script-src
setzt die erlaubten Quellen für JavaScriptstyle-src
setzt die erlaubten Quellen für CSSimg-src
setzt die erlaubten Quellen für Bilder
default-src
fungiert hierbei als Fallback für alle Ressourcen, für die es keine explizite Direktive gibt.
Eine Direktive kann Nonces sowie auch Hostnamen enthalten. Auf Nonces wird im nächsten Abschnitt eingegangen. Zudem ist es möglich, einen Ressourcentyp vollständig zu blockieren. Hierzu wird das Stichwort none
verwendet. Dieser Wert kann nicht zusammen mit anderen Direktivenwerten verwendet werden. Ist dies Trotzdem der Fall, werden die anderen ignoriert.
Eine Liste mit allen Direktiven und deren Source expressions lässt sich auf der mdn web docs Webseite finden.
Nonces
Nonces sind der empfohlene Weg, um das Laden von Skripten und CSS-Dateien zu unterbinden. Nonce steht für number used once und wie der Name schon sagt, muss sich der Wert bei jedem HTTP-Aufruf ändern. Der Wert erscheint dann in einer script-src
oder style-src
Direktive.
Der Header könnte dann wie folgt aussehen:
Content-Security-Policy: script-src 'nonce-416d1177-4d12-4e3b-b7c9-f6c409789fb8'
Und die eingebundenen <script>
-Elemente entsprechend so:
<script nonce="416d1177-4d12-4e3b-b7c9-f6c409789fb8" src="/main.js"></script>
<script nonce="416d1177-4d12-4e3b-b7c9-f6c409789fb8">console.log("hello!");</script>
Der Nonce-Wert (ohne den Präfix nonce-) muss als Wert des Nonce-Attributes gesetzt sein. Nur bei einer Übereinstimmung dieses Wertes und des Wertes im Response-Header wird das Skript geladen und ausgeführt.
Die Idee dahinter ist, dass selbst wenn ein Angreifer JavaScript in die Seite einfügen kann, er nicht weiss, welchen Nonce der Server verwenden wird, sodass der Browser die Ausführung des Skripts verweigert. Dementsprechend versteht es sich von selbst, dass die Nonce-Werte nicht vorhersagbar sein dürfen.
Wird als serverseitige Sprache beispielsweise PHP verwendet, könnte man sich ein CSP-Nonce wie folgt generieren:
bin2hex(openssl_random_pseudo_bytes(32))
Es gilt zu beachten, dass der Server:
- für jeden Seitenaufruf einen neuen Nonce generiert
- die Nonces für inline, sowie externe Skripte verwenden kann
- den selben Nonce für alle Skripte verwendet
Es ist wichtig, dass der Server eine Art von Template verwendet, um Nonces einzufügen, und diese nicht einfach in alle <script>
-Tags einfügt: Andernfalls könnte der Server versehentlich Nonces in Skripte einfügen, die von einem Angreifer injiziert wurden.
Beachten Sie, dass Nonces nur für Elemente verwendet werden können, die über ein nonce
-Attribut verfügen, d. h. nur für <script>
– und <style>
-Elemente.
Hashes
Hashes sind ein weiterer Mechanismus, um die Integrität der geladenen JavaScript-Dateien zu gewährleisten.
Hierbei wird ein Hash auf Basis des Inhalts des Skriptes mit Hilfe einer kryptografischen Hash-Funktion berechnet (SHA-256, SHA-384 oder SHA-512). Danach wird das Resultat base64-enkodiert und dieses im CSP-Header und im integrity
-Attribut aufgeführt.
Content-Security-Policy: script-src 'sha256-ex2O7MWOzfczthhKm6azheryNVoERSFrPrdvxRtP8DI=' '
sha256-H/eahVJiG1zBXPQyXX0V6oaxkfiBdmanvfG9eZWSuEc='
<script src="./main.js" integrity="sha256-H/eahVJiG1zBXPQyXX0V6oaxkfiBdmanvfG9eZWSuEc="></script>
<script>console.log("hello!");</script>
Wenn der Browser das Dokument empfängt, hasht er das Skript, vergleicht das Ergebnis mit dem Wert aus dem Header und lädt das Skript nur, wenn beide übereinstimmen.
Externe Skripte müssen ebenfalls das Attribut integrity
enthalten, damit diese Methode funktioniert.
Es gibt Folgendes zu beachten:
- Es muss für jedes Skript im Dokument ein separater Hash existieren. Die Hashes werden im Header mit einem Abstand getrennt aufgelistet.
- Im Gegensatz zum Beispiel mit Nonces können sowohl die CSP als auch der Inhalt statisch sein, da die Hashes unverändert bleiben. Dadurch eignen sich hashbasierte Richtlinien besser für statische Seiten oder Websites, die auf clientseitiges Rendering setzen.
Inline JavaScript
Wenn ein CSP entweder eine default-src
– oder eine script-src
-Anweisung enthält, darf Inline-JavaScript nur ausgeführt werden, wenn zusätzliche Massnahmen zu seiner Aktivierung ergriffen werden.
Inline-<script>
-Elemente sind zulässig, wenn sie durch einen Nonce oder einen Hash geschützt sind.
JavaScript Code innerhalb eines <script>
-Elementes:
<script>
console.log("Hello from an inline script");
</script>
JavaScript Code innerhalb eines event handler Attributes:
<img src="x" onerror="console.log('Hello from an inline event handler')" />
JavaScript innerhalb einerr javascript:
URL:
<a href="javascript:console.log('Hello from a javascript: URL')"></a>
Das Schlüsselwort unsafe-inline
kann verwendet werden, um diese Einschränkung zu überschreiben. Die folgende Anweisung verlangt beispielsweise, dass alle Ressourcen denselben Ursprung haben, erlaubt jedoch Inline-JavaScript. Entwickler sollten unsafe-inline
vermeiden, da es einen Großteil des Zwecks von CSP zunichte macht.
❌ Content-Security-Policy: default-src 'self' 'unsafe-inline' ❌
Wenn eine Direktive Nonce- oder Hash-Ausdrücke enthält, wird das Schlüsselwort unsafe-inline
von Browsern ignoriert.
Refactoring von Inline-JavaScript
Inline-JavaScript ist in einer CSP grundsätzlich nicht erlaubt. Mit Nonces oder Hashes kann ein Entwickler Inline <script>
-Elemente verwenden, aber man muss dennoch den Code umgestalten, um andere unzulässige Muster zu entfernen, darunter inline event handler, javascript:
-URLs und die Verwendung von eval()
. Beispielsweise sollten inline event handler in der Regel durch Aufrufe von addEventListener() ersetzt werden.
Von diesem Code:
<p onclick="console.log('Hello from an inline event handler')">click me</p>
Zu diesem:
<!-- served with the following CSP:
`script-src 'sha256-AjYfua7yQhrSlg807yyeaggxQ7rP9Lu0Odz7MZv8cL0='`
-->
<p id="hello">click me</p>
<script>
const hello = document.querySelector("#hello");
hello.addEventListener("click", () => {
console.log("Hello from an inline script");
});
</script>
Strict CSP vs. Allowlist CSP
Um die Gefahr von XSS-Angriffen zu mitigieren, wird empfohlen, auf Nonce- oder Hash-basierte Direktiven zurückzugreifen. Dies wird als strict-CSP bezeichnet. Diese Art von CSP hat gegenüber einer location-based CSP (in der Regel als Allowlist-CSP bezeichnet) zwei wesentliche Vorteile:
- Allowlist-CSP’s sind schwer richtig einzurichten, und oft werden durch die Richtlinien versehentlich unsichere Domains auf die Whitelist gesetzt, sodass kein wirksamer Schutz vor XSS gewährleistet ist.
- Allowlist-CSP’s können sehr umfangreich und schwer zu verwalten sein, insbesondere wenn Skripte verwendet werden, die ausserhalb der eigenen Kontrolle liegen.
Die strict CSP ist schwer umzusetzen, wenn Skripte verwendet werden, die nicht unter der eigenen Kontrolle stehen. Wenn ein Skript eines Drittanbieters zusätzliche Skripte lädt oder Inline-Skripte verwendet, schlägt dies fehl, da das Skript des Drittanbieters den Nonce oder Hash nicht weitergibt.
Eine Nonce basierte strict CSP könnte in etwa so aussehen:
Content-Security-Policy:
script-src 'nonce-{RANDOM}';
object-src 'none';
base-uri 'none';
In diesem Beispiel werden:
- Nonces verwendet, um zu definieren, welche JavaScript-Ressourcen geladen werden dürfen.
- alle Verwendungen des <object>-Elements unterbunden
- alle Verwendungen des <base>-Elements unterbunden
Eine Hash-basierte strict CSP verhält sich ähnlich, mit dem Unterschied, dass Hashes und keine Nonces zum Einsatz kommen:
Content-Security-Policy:
script-src 'sha256-{HASHED_SCRIPT}';
object-src 'none';
base-uri 'none';
Nonce-basierte Direktiven sind einfacher zu verwalten, wenn der Inhalt dynamisch generiert werden kann. Andernfalls muss auf Hash-basierte Direktiven zurückgegriffen werden. Das Problem bei Hash-basierten Direktiven ist, dass der Hash erneut berechnet und eingefügt werden muss, wenn Änderungen am Skriptinhalt vorgenommen werden.
Die Direktive „strict-dynamic“
Sofern ein Skript mit einem Hash oder Nonce verknüpft ist, kann der Wert strict-dynamic in Direktiven angegeben werden, um dem Skript zu erlauben, weitere Skripte zu laden, welche wiederum über keine Hashes oder Nonces verfügen.
Das heisst, das Vertrauen, das ein Nonce oder Hash einem Skript entgegenbringt, wird an Skripte weitergegeben, die das ursprüngliche Skript lädt (und an Skripte, die diese wiederum laden, und so weiter).
Content-Security-Policy:
script-src 'sha256-gEh1+8U9S1vkEuQSmmUMTZjyNSu5tIoECP4UXIEjMTk='
strict-dynamic
Das Schlüsselwort strict-dynamic
erleichtert die Erstellung und Pflege von Nonce- oder Hash-basierten CSP’s erheblich, insbesondere wenn eine Webseite Skripte von Drittanbietern verwendet. Allerdings wird diese dadurch weniger sicher.
Wenn strict-dynamic
vorhanden ist, ist Host basiertes erlauben nicht möglich.
Platzhalter
Wenn nicht klar ist, woher die Dateien überall stammen, kann mit Platzhaltern gearbeitet werden.
Die folgende Direktive erlaubt nur Bilder, die denselben Ursprung wie das aktuelle Dokument haben oder von einer Subdomain von „example.org“ bereitgestellt werden oder von „example.com“ bereitgestellt werden.
Content-Security-Policy: img-src 'self' *.example.org example.com
Probelauf mit „report-only“
Um die Bereitstellung zu vereinfachen, kann die CSP im sogenannten report-only Modus bereitgestellt werden. Die Richtlinie wird nicht durchgesetzt, aber alle Verstösse werden an den in der Richtlinie angegebenen Endpunkten gesendet. Darüber hinaus kann ein reiner report-only Header verwendet werden, um eine zukünftige Überarbeitung einer Richtlinie zu testen, ohne sie tatsächlich bereitzustellen.
Man kann den HTTP-Header Content-Security-Policy-Report-Only
verwenden, um die Richtlinie festzulegen. Wenn sowohl ein Content-Security-Policy-Report-Only
als auch ein Content-Security-Policy
Header präsent sind, werden beide Richtlinien berücksichtigt.
Wie weiter oben schon erwähnt, hat die Variante, welche ein meta Element verwendet, gewisse Einschränkungen. Eine report-only policy kann bspw. nicht über ein solches Element übertragen werden. Zudem kann es sein, dass der Browser JavaScript ausführt, bevor die CSP angewendet wird, wenn mit einem meta Element gearbeitet wird, da die Policy erst nach dem parsen des HTML in Kraft tritt.
Meldung von Verstössen
Um bei einer aktiven CSP über Richtlinienverstösse informiert zu werden, wird die Verwendung der Reporting API empfohlen. Es werden mittels Reporting-Endpoints
Endpunkte definiert und diese im Content-Security-Policy
Header referenziert. Die URL’s werden als komma-separierte Liste angegeben.
Um beispielsweise einen Endpunkt namens „csp-endpoint“ zu definieren, der Berichte unter „https://example.com/csp-reports“ akzeptiert, könnte der Antwortheader des Servers wie folgt aussehen:
Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"
Falls mehrere Endpunkte genutzt werden sollen, dann würde die Definition so lauten:
Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports",
hpkp-endpoint="https://example.com/hpkp-reports"
Es gibt noch eine weitere Möglichkeit, welche die CSP-Direktive report-uri
verwendet. Dabei wird eine Ziel-URL angegeben, an die die Berichte über CSP-Verstösse gesendet werden. Dadurch wird ein etwas anderes JSON-Format über eine POST-Anfrage mit dem Content-Type application/csp-report
gesendet. Dieser Ansatz ist veraltet, es sollten aber beide Varianten verwendet werden, bis „report-to“ in allen Browsern unterstützt wird.
Ein angepasstes Beispiel könnte so aussehen:
Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"
Content-Security-Policy: default-src 'self'; report-to csp-endpoint
Wenn nun ein CSP-Verstoss eintritt, sendet der Browser den Bericht als JSON-Objekt and die hinterlegten Endpunkte mittels einer POST-Anfrage mit dem Content-Type application/reports+json.
Ein typisches Objekt könnte wie folgt aussehen (mit „report-to”):
{
"age": 53531,
"body": {
"blockedURL": "inline",
"columnNumber": 39,
"disposition": "enforce",
"documentURL": "https://example.com/csp-report",
"effectiveDirective": "script-src-elem",
"lineNumber": 121,
"originalPolicy": "default-src 'self'; report-to csp-endpoint-name",
"referrer": "https://www.google.com/",
"sample": "console.log(\"lo\")",
"sourceFile": "https://example.com/csp-report",
"statusCode": 200
},
"type": "csp-violation",
"url": "https://example.com/csp-report",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36"
}
Oder so (mit report-uri)
"csp-report": {
"blocked-uri": "inline",
"column-number": 51,
"disposition": "report",
"document-uri": "https://www.example.com/",
"effective-directive": "script-src-elem",
"line-number": 149,
"original-policy": "default-src 'self'; script-src 'report-sample' 'self' https://*.googletagmanager.com https://*.googleapis.com https://www.youtube.com https://www.google.com https://www.gstatic.com https://www.google-analytics.com; font-src 'self' data: https://www.autoneum.com https://fonts.gstatic.com https://irs.tools.investis.com; img-src 'self' data: https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com https://*.g.doubleclick.net https://*.google.com; connect-src 'self' https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com https://*.g.doubleclick.net https://*.google.com; frame-src 'self' https://td.doubleclick.net https://www.googletagmanager.com https://www.google.com https://irs.tools.investis.com; report-uri https://www.example.com/wp-content/themes/grand-sunrise/csp.php",
"referrer": "",
"script-sample": "\n\t( function() {\n\t\tvar skipLinkTarget = \u2026",
"source-file": "https://www.example.com/",
"status-code": 200,
"violated-directive": "script-src-elem"
}
Von Interesse ist im zweiten JSON vor allem der Wert des script-sample
Schlüssels. Darin sind die ersten 40 Zeichen des Inline-Skripts, event handlers oder Stils, die den Verstoss verursacht haben, ersichtlich. Verstösse, die aus externen Dateien stammen, werden nicht in den Bericht aufgenommen.
Dieser Schlüssel existiert jedoch nur, wenn in den script-src
oder style-src
Direktiven der Wert report-sample
vorkommt.
Der Server muss so eingerichtet sein, dass er Berichte mit dem angegebenen JSON-Format und Inhaltstyp empfangen kann. Dieser kann die eingehenden Berichte dann so speichern oder verarbeiten, wie es den Anforderungen am besten entspricht.
Testen der Policy
Nebst den schon in modernen Webbrowsern eingebauten Features lassen sich im Web etliche Werkzeuge und Validatoren finden, mit deren Hilfe man eine CSP von Grund auf generieren oder auf syntaktische Korrektheit überprüfen lassen kann.
Anbei ein paar nützliche Seiten:
- https://csp-evaluator.withgoogle.com/
- https://report-uri.com/home/analyse
- https://report-uri.com/home/generate
Weitere sicherheitsrelevante HTTP-Header
Mit einer CSP ist die Arbeit aber noch nicht getan. Es gibt eine Fülle weiterer sicherheitsrelevanter HTTP-Header, mit denen eine Webseite gehärtet werden kann. Untenstehend ein kleiner Ausschnitt:
- Strict-Transport-Security: Erzwingt die Nutzung von HTTPS für eine Bestimmte Dauer.
- X-Frame-Options: Verhindert das Einbetten der Seite in ein iFrame. Die Verwendung dieses Headers wird generell nicht mehr empfohlen; stattdessen sollte die CSP-Direktive
frame-ancestors
bevorzugt werden. - Referrer-Policy: Teilt dem Browser mit, welche Referrer-Informationen gesendet werden dürfen.
- Access-Control-Allow-Origin: Gibt an, welche Domains auf gewisse Ressourcen – bspw. Skripte – zugreifen dürfen.