Zum Inhalt springen

Leitfaden

Content Security Policy und Security-Header richtig einrichten

Von Lucas Weber, CyberSec42 · Stand 28.10.2026

CyberSec42

Eine Content Security Policy (CSP) ist ein HTTP-Header, mit dem Ihre Website dem Browser vorschreibt, aus welchen Quellen er Skripte, Styles, Bilder und Frames laden darf. Richtig eingerichtet verhindert sie, dass eingeschleustes JavaScript ausgeführt wird. Zusammen mit vier weiteren Headern, nämlich Strict-Transport-Security, X-Content-Type-Options, Referrer-Policy und Permissions-Policy, bildet sie die Grundausstattung, die jede Website auf jeder Antwort ausliefern sollte, nicht nur auf der Startseite.

Kurz gefasst.

Die Header im Überblick

Was jeder Header tut und welcher Wert passt.

Zum Abschnitt

CSP aufbauen

Direktiven, Hashes, Nonces, frame-ancestors.

Zum Abschnitt

Unser eigenes Beispiel

Die gemessenen Header von cybersec42.com.

Zum Abschnitt

Konfiguration

Vercel, Netlify, Apache und nginx.

Zum Abschnitt

Security-Header prüfen

GET statt HEAD, Konsole, Report-Only.

Zum Abschnitt

Häufige Fehler

Von unsafe-inline bis Header nur auf der Startseite.

Zum Abschnitt

Die Security-Header im Überblick

Security-Header sind Anweisungen, die Ihr Server mit jeder Antwort an den Browser schickt. Sie ändern nichts an Ihrem Code, sie schränken nur ein, was der Browser mit Ihrer Seite tun darf. Das ist ihre Stärke und ihre Grenze: Sie mildern ganze Fehlerklassen ab, etwa eingeschleuste Skripte oder das Einbetten Ihrer Seite in einen fremden Frame, beheben aber keine Lücke in der Anwendung selbst.

HeaderWofürÜblicher Startwert
Content-Security-PolicyErlaubte Quellen für Skripte, Styles, Bilder, Verbindungen und Frames; Schutz gegen eingeschleustes JavaScript und Klickjackingindividuell, siehe unten
Strict-Transport-SecurityBrowser ruft die Domain nur noch per HTTPS auf (HSTS)max-age=31536000; includeSubDomains
X-Content-Type-OptionsBrowser rät keinen Dateityp, wenn der angegebene nicht passtnosniff
Referrer-PolicyWie viel der eigenen URL an andere Seiten weitergegeben wirdstrict-origin-when-cross-origin
Permissions-PolicyBrowser-Funktionen wie Kamera, Mikrofon oder Standort abschaltengeolocation=(), microphone=(), camera=()

Die Werte in der Tabelle entsprechen den Empfehlungen im OWASP HTTP Headers Cheat Sheet, bei HSTS mit einem Jahr statt der dort genannten zwei Jahre und ohne preload als vorsichtigerem Einstieg. Die einzelnen Header im Detail:

Strict-Transport-Security (HSTS)

HSTS ist in RFC 6797 festgelegt. max-age gibt in Sekunden an, wie lange der Browser Ihre Domain nur noch per HTTPS anspricht. includeSubDomains dehnt das auf alle Subdomains aus. Setzen Sie das erst, wenn wirklich jede Subdomain ein gültiges Zertifikat hat, denn bei einem Zertifikatsfehler auf einem HSTS-Host bietet der Browser laut MDN keine Möglichkeit mehr, trotzdem weiterzugehen. Zwei Details aus der RFC: Ein HSTS-Header, der über unverschlüsseltes HTTP ankommt, wird ignoriert. Und max-age=0 hebt die Regel wieder auf, allerdings erst, wenn der Browser die Seite erneut per HTTPS abruft.

Der Zusatz preload steht nicht in der RFC. Er gehört zur Preload-Liste, die über hstspreload.org gepflegt wird und in Browser eingebaut ist. Voraussetzungen dort: max-age von mindestens einem Jahr, includeSubDomains und preload. Die Seite warnt ausdrücklich, dass eine Austragung Monate dauert, bis sie bei den Nutzern ankommt. Tragen Sie Ihre Domain deshalb nur ein, wenn Sie sicher sind.

X-Content-Type-Options

Der einzige Wert ist nosniff. Laut MDN blockiert der Browser dann Stylesheets, die nicht als text/css ausgeliefert werden, und Skripte ohne JavaScript-Dateityp. Voraussetzung ist, dass Ihr Server für alle Dateien den richtigen Content-Type setzt.

Referrer-Policy

strict-origin-when-cross-origin ist laut W3C-Spezifikation und MDN heute der Standard der Browser, wenn nichts gesetzt ist: innerhalb Ihrer Seite die volle URL, an andere HTTPS-Seiten nur die Domain, an HTTP-Seiten gar nichts. OWASP empfiehlt trotzdem, den Wert ausdrücklich zu setzen, statt sich auf Browser-Voreinstellungen zu verlassen. Wer URLs mit Tokens oder Suchbegriffen hat, kann strenger werden, etwa mit no-referrer.

Permissions-Policy

Mit einer leeren Liste () schalten Sie eine Funktion für Ihre Seite und alle eingebetteten Frames ab, mit (self) erlauben Sie sie nur der eigenen Herkunft. Schalten Sie alles ab, was Ihre Seite nicht braucht. MDN führt den Header als noch nicht in allen verbreiteten Browsern verfügbar. Er ist daher eine zusätzliche Schicht, auf die Sie sich nicht allein verlassen sollten.

X-Frame-Options und X-XSS-Protection

X-Frame-Options verhindert, dass Ihre Seite in einem fremden Frame erscheint. Die CSP-Spezifikation (Level 3) hält fest, dass frame-ancestors diesen Header überstimmt. frame-ancestors 'none' entspricht laut MDN X-Frame-Options: DENY, ist aber flexibler, weil Sie einzelne erlaubte Domains angeben können. X-Frame-Options: DENY zusätzlich zu senden schadet nicht und deckt ältere Browser ab.

X-XSS-Protection dagegen lassen Sie weg. MDN führt den Header als veraltet und nicht standardisiert und warnt, dass der Filter in manchen Fällen selbst XSS-Lücken erzeugen konnte. Auch OWASP rät, ihn nicht zu setzen oder ausdrücklich mit 0 abzuschalten und stattdessen auf eine CSP ohne Inline-JavaScript zu setzen.

Die Content Security Policy aufbauen

Eine CSP besteht aus Direktiven, getrennt durch Semikolon. Jede Direktive nennt einen Ressourcentyp und die erlaubten Quellen. default-src gilt für alle Typen, die nicht eigens aufgeführt sind. Ein solider Ausgangspunkt für eine Website ohne fremde Skripte:

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self';
  img-src 'self' data:;
  font-src 'self';
  connect-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none'

Im Header steht das in einer Zeile, die Umbrüche dienen hier nur der Lesbarkeit. Die wichtigsten Direktiven:

Inline-Skripte erlauben, ohne unsafe-inline

Viele Seiten brauchen ein paar Inline-Skripte. Statt 'unsafe-inline' erlauben Sie genau diese Skripte, und zwar auf zwei Arten:

MDN nennt beide Wege eine „strikte CSP“ und die empfohlene Methode gegen XSS. Wichtig für die Umstellung: Enthält eine Direktive einen Hash oder eine Nonce, ignorieren Browser laut MDN und CSP-Spezifikation ein dort ebenfalls stehendes 'unsafe-inline'. Den Hash berechnen Sie über den exakten Inhalt zwischen den Script-Tags, Leerzeichen und Zeilenumbrüche zählen mit:

# inline.js enthält exakt den Text zwischen <script> und </script>
openssl dgst -sha256 -binary inline.js | openssl base64 -A

Inline-Event-Handler wie onclick="…" lassen sich auf diese Weise nicht einfach erlauben. Verlagern Sie sie in eine Skriptdatei mit addEventListener. Für Seiten, die viele Skripte nachladen, gibt es 'strict-dynamic': Ein per Hash oder Nonce erlaubtes Skript darf dann weitere Skripte laden. MDN weist darauf hin, dass das die Policy weniger streng macht, wenn diese Skripte Inhalte aus unsicheren Quellen verarbeiten.

So machen wir es auf dieser Seite

Gemessen am 28.10.2026 per GET mit curl -s -D - -o /dev/null https://www.cybersec42.com/, gegengeprüft auf einer zweiten URL mit Cache-Buster. Beide Antworten trugen dieselben Security-Header. Die CSP ist gekürzt, sie enthält 14 Hashes:

content-security-policy: default-src 'self';
  script-src 'self' 'sha256-2TkQLINVURTi/RmDX37prpPrw8Zm7dkRfIzOPWxyCG0='
    'sha256-3mC/Lyy+nMH9q6flJmQx73Ahi1EFl8by8yb2zwfjzNU=' … (14 Hashes);
  style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';
  connect-src 'self'; frame-ancestors 'none'; base-uri 'self';
  form-action 'self'; object-src 'none'
strict-transport-security: max-age=63072000; includeSubDomains; preload
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=(), interest-cohort=()

Die Seite ist statisch und wird über Vercel ausgeliefert. Die Header stehen in der vercel.json mit "source": "/(.*)", gelten also für jede Datei. Ein kleines Build-Skript sucht vor jedem Deploy alle Inline-Skripte im Bundle, berechnet ihre SHA-256-Hashes und schreibt sie in script-src. Strukturierte Daten in <script type="application/ld+json"> sind reine Datenblöcke, werden nicht ausgeführt und brauchen keinen Hash. Inline-Event-Handler gibt es nicht. Ändert sich ein Inline-Skript, ohne dass das Skript neu läuft, blockiert der Browser es. Genau das wollen wir: Was nicht ausdrücklich erlaubt ist, läuft nicht.

Zwei bewusste Entscheidungen sehen Sie in der Messung. style-src enthält 'unsafe-inline', weil die Seiten Stilangaben im HTML nutzen. Für Styles ist das ein kleineres Restrisiko als für Skripte, aber kein null Risiko, daher steht es auf unserer Liste. Und wir senden X-Frame-Options: DENY zusätzlich zu frame-ancestors 'none', als Rückfall für ältere Browser.

Konfigurationsbeispiele für Vercel, Netlify, Apache und nginx

Die Beispiele setzen dieselben Header wie die Tabelle oben. Die CSP ist der Ausgangspunkt aus dem vorigen Abschnitt, passen Sie sie an Ihre Seite an und testen Sie sie zuerst im Report-Only-Modus.

Vercel: vercel.json

Laut Vercel-Dokumentation legen Sie Header im Feld headers fest, je Pfadmuster unter source. /(.*) trifft alle Pfade.

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "Content-Security-Policy", "value": "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" },
        { "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains" },
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
        { "key": "Permissions-Policy", "value": "geolocation=(), microphone=(), camera=()" }
      ]
    }
  ]
}

Netlify: _headers

Die Datei _headers gehört in das Verzeichnis, das Netlify veröffentlicht, also das Build-Ergebnis. Die Netlify-Dokumentation weist auf eine Grenze hin: Diese Header gelten nur für Dateien, die Netlify selbst ausliefert, nicht für Antworten aus Serverless- oder Edge-Functions und nicht für weitergeleitete Inhalte. Dort muss die Funktion die Header selbst setzen.

/*
  Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'
  Strict-Transport-Security: max-age=31536000; includeSubDomains
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: geolocation=(), microphone=(), camera=()

Apache: .htaccess

Apache setzt Header über mod_headers. In einer .htaccess funktioniert das laut Apache-Dokumentation nur mit AllowOverride FileInfo. Verwenden Sie Header always set: Ohne always landen die Header nicht auf Weiterleitungen und Fehlerseiten.

Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"

nginx

In nginx gilt add_header laut Dokumentation ohne den Zusatz always nur für erfolgreiche Antworten und Weiterleitungen, nicht für Fehlerseiten. Die größere Falle ist die Vererbung: Header aus dem server-Block gelten in einem location-Block nur, wenn dort kein eigenes add_header steht. Ein einzelnes add_header Cache-Control … in einer Location lässt dort alle Security-Header verschwinden. Ab nginx 1.29.3 lässt sich das mit add_header_inherit merge; ändern, sonst wiederholen Sie die Header in jeder Location oder binden sie per include ein.

server {
    # ...
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
}

Security-Header prüfen: so messen Sie richtig

Prüfen Sie nur Websites, die Ihnen gehören oder für die Sie eine Erlaubnis haben. Für das Abrufen der eigenen Header genügen Bordmittel.

Schritt 1: Header per GET abrufen, nicht per HEAD

Der naheliegende Befehl curl -I schickt eine HEAD-Anfrage. Laut RFC 9110 soll ein Server darauf dieselben Header senden wie bei GET, er darf aber Header weglassen, deren Wert erst beim Erzeugen des Inhalts feststeht. Genau dort setzen manche Anwendungen ihre Security-Header. Messen Sie deshalb mit einer echten GET-Anfrage und verwerfen Sie nur den Inhalt:

curl -s -D - -o /dev/null https://www.example.com/

Prüfen Sie danach mehrere Seitentypen auf einmal, darunter eine Unterseite, eine Datei und eine Fehlerseite. Ein angehängter Parameter wie ?cb=1 umgeht zwischengespeicherte Antworten. Vergleichen Sie zusätzlich ohne Parameter, denn das ist die Antwort, die Ihre Besucher bekommen:

for p in / /kontakt.html /gibt-es-nicht /favicon.ico; do
  echo "== $p"
  curl -s -D - -o /dev/null "https://www.example.com$p" \
    | grep -i -E '^(content-security-policy|strict-transport-security|x-content-type-options|referrer-policy|permissions-policy)'
done

Schritt 2: Browser-Konsole lesen

Öffnen Sie die Entwicklerwerkzeuge (F12) und laden Sie die Seite neu. Im Reiter „Netzwerk“ sehen Sie bei der ersten Anfrage die tatsächlich angekommenen Antwort-Header. Im Reiter „Konsole“ meldet der Browser jede Ressource, die die CSP blockiert hat, mit der verletzten Direktive. Klicken Sie danach durch die wichtigen Funktionen: Formular absenden, Menü öffnen, Video starten, Cookie-Hinweis bestätigen. Viele Verstöße treten erst bei einer Aktion auf.

Schritt 3: Neue Policy zuerst im Report-Only-Modus

Mit Content-Security-Policy-Report-Only wird die Policy nicht durchgesetzt, der Browser meldet Verstöße nur in der Konsole und an einen Endpunkt, wenn Sie einen angeben. Laut MDN können beide Header gleichzeitig gesetzt sein: Die bestehende Policy wirkt weiter, die neue wird nur getestet. Für Berichte empfiehlt MDN report-to mit dem Header Reporting-Endpoints. Das ältere report-uri gilt laut CSP-Spezifikation als veraltet, MDN rät aber, beide anzugeben, solange report-to nicht in allen Browsern unterstützt wird.

Reporting-Endpoints: csp="https://www.example.com/csp-berichte"
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; report-to csp; report-uri https://www.example.com/csp-berichte

Lassen Sie den Report-Only-Modus laufen, bis keine neuen berechtigten Verstöße mehr auftauchen, und übernehmen Sie die Policy dann in Content-Security-Policy. Für einen schnellen Überblick über die öffentlich sichtbaren Einstellungen gibt es kostenlose Prüfwerkzeuge, die wir im 15-Minuten-Check für Ihre Website einordnen.

Die häufigen Fehler

1. unsafe-inline in script-src

Der häufigste Grund, warum eine vorhandene CSP kaum schützt. Oft wurde 'unsafe-inline' ergänzt, weil nach dem Einschalten etwas nicht mehr funktionierte. Ersetzen Sie es durch Hashes oder Nonces und verlagern Sie Inline-Event-Handler in Skriptdateien. Dasselbe gilt für 'unsafe-eval', das laut MDN anders als 'unsafe-inline' auch neben Hashes und Nonces wirksam bleibt.

2. Zu breite Quellen

Einträge wie https:, * oder ganze Domains großer Anbieter mit vielen Kunden erlauben weit mehr als die eine Datei, die Sie brauchen. Liegt auf einer erlaubten Domain beliebiger fremder Inhalt, kann ein Angreifer sein Skript von dort laden. Nennen Sie so konkrete Quellen wie möglich, und bei script-src lieber Hashes oder Nonces als Domain-Listen.

3. CSP nur als Meta-Tag

Eine CSP lässt sich auch über <meta http-equiv="Content-Security-Policy"> setzen. Laut CSP-Spezifikation wirken dort aber frame-ancestors, report-uri und sandbox nicht, und der Report-Only-Modus wird im Meta-Tag gar nicht unterstützt. Ein Meta-Tag schützt also nicht gegen Klickjacking. Wenn Sie auf den Server oder das Hosting Zugriff haben, setzen Sie die CSP als HTTP-Header.

4. Header nur auf der Startseite

Header, die im Code einer bestimmten Seite oder in einem einzelnen location-Block gesetzt werden, fehlen auf allen anderen Antworten. Typische Lücken: Unterseiten mit eigener Regel, Fehlerseiten ohne always, Dateien aus einem anderen Verzeichnis oder einem CDN, Antworten aus Serverless-Functions bei Netlify. Die Schleife aus Schritt 1 deckt das in einer Minute auf.

5. HSTS zu früh mit preload

Wer includeSubDomains; preload setzt und die Domain einträgt, bevor alle Subdomains HTTPS können, sperrt Besucher von diesen Subdomains aus, und die Austragung dauert Monate. Beginnen Sie ohne preload und erweitern Sie, wenn alles läuft.

6. Veraltete Header als Ersatz

X-XSS-Protection ersetzt keine CSP, und X-Frame-Options: ALLOW-FROM ignorieren aktuelle Browser laut MDN vollständig. Für erlaubte Einbettungen brauchen Sie frame-ancestors mit den konkreten Domains.

Checkliste

BereichMaßnahme
CSPAls HTTP-Header, mit default-src, script-src ohne 'unsafe-inline', object-src 'none', base-uri, form-action, frame-ancestors
Inline-SkriptePer Hash oder Nonce erlaubt, keine Inline-Event-Handler
QuellenSo konkret wie möglich, kein * und kein https: in script-src
HSTSmax-age mindestens ein Jahr, includeSubDomains erst nach Prüfung aller Subdomains, preload nur bewusst
Weitere Headernosniff, Referrer-Policy ausdrücklich gesetzt, Permissions-Policy für ungenutzte Funktionen
AltlastenX-XSS-Protection entfernt oder 0, kein ALLOW-FROM
AbdeckungHeader auf Unterseiten, Dateien und Fehlerseiten per GET gemessen
EinführungNeue CSP zuerst im Report-Only-Modus, Konsole bei allen wichtigen Funktionen sauber

Was Header nicht leisten

Security-Header sind eine zweite Verteidigungslinie. Eine CSP verhindert, dass eingeschleuste Skripte laufen, sie verhindert nicht, dass jemand über eine Lücke in Ihrem Formular, Ihrer API oder Ihrem Login an Daten kommt. Ob Ihre Anwendung solche Lücken hat, zeigt erst eine Prüfung der Anwendung selbst. Werden dabei personenbezogene Daten tatsächlich abgerufen, kann das eine Meldepflicht nach Art. 33 DSGVO auslösen. Ob das im Einzelfall zutrifft, ist eine Rechtsfrage. Dieser Artikel ist keine Rechtsberatung.

Sind Ihre Header wirklich wirksam?

Wir prüfen Ihre Website im schriftlich vereinbarten Umfang: ausgelieferte Security-Header auf allen Seitentypen, die CSP gegen die tatsächlich geladenen Skripte, Einbettung und Weiterleitungen, dazu Formulare, Anmeldung und Schnittstellen. Jeder Befund kommt mit einem nachvollziehbaren Nachweis und einem konkreten Korrekturvorschlag für Ihr Hosting. Mehr dazu unter Penetrationstest.

Website-Sicherheitscheck anfragen

CyberSec42 ist eine unabhängige technische Sicherheitsprüfung, keine akkreditierte Zertifizierungsstelle und keine Rechtsberatung. CyberSec42 steht in keiner Verbindung zu Vercel, Netlify, Apache oder nginx.

Häufige Fragen

Reicht eine Content Security Policy als Meta-Tag?

Für den Schutz vor eingeschleusten Skripten wirkt sie auch als Meta-Tag. frame-ancestors, report-uri, sandbox und der Report-Only-Modus funktionieren dort aber nicht. Gegen Klickjacking brauchen Sie den HTTP-Header.

Brauche ich X-Frame-Options noch?

frame-ancestors in der CSP überstimmt X-Frame-Options laut CSP-Spezifikation und ist flexibler. X-Frame-Options: DENY zusätzlich zu senden schadet nicht und deckt ältere Browser ab.

Soll ich X-XSS-Protection setzen?

Nein. MDN führt den Header als veraltet, und OWASP rät, ihn wegzulassen oder mit 0 abzuschalten. Den Schutz übernimmt eine CSP ohne 'unsafe-inline' in script-src.

Warum zeigt curl -I andere Header als der Browser?

curl -I sendet eine HEAD-Anfrage, bei der ein Server laut RFC 9110 Header weglassen darf, die erst beim Erzeugen der Seite entstehen. Dazu kommen Caches, die ältere Antworten ausliefern. Messen Sie per GET mit curl -s -D - -o /dev/null, mit und ohne Cache-Buster.

Kann eine CSP meine Website kaputt machen?

Ja, wenn sie Skripte, Schriften oder Verbindungen blockiert, die Ihre Seite braucht. Deshalb starten Sie mit Content-Security-Policy-Report-Only, lesen die Konsole und Berichte und schalten erst danach scharf.

Ersetzen gute Security-Header einen Penetrationstest?

Nein. Header mildern bestimmte Angriffe im Browser ab. Lücken in Anmeldung, Rechteprüfung, Formularen oder Schnittstellen finden sie nicht. Dafür braucht es eine Prüfung der Anwendung selbst.

Weiterlesen

Quellen