Zum Inhalt springen
Illustration: Ein Server schickt eine Reihe flacher Karten mit grünen Schildern zu einem Browserfenster.
Security

Security-Header einrichten: HSTS, CSP und vier weitere

Von · · 25 Min. Lesezeit Zuletzt aktualisiert am

Von sechs Security-Headern, die ich am 17.09.2026 testweise per <meta>-Tag ins HTML geschrieben habe, wirkten in Chrome zwei. Die anderen vier gehören in die Antwort des Servers, sonst passiert nichts. Security-Header sind solche Zeilen in der HTTP-Antwort, die dem Browser Regeln mitgeben: nur HTTPS, keine fremden Skripte, keine Einbettung in fremde Rahmen. Sechs davon lohnen sich für fast jede Website, und dieser Beitrag nennt für jeden den Startwert, die Konfiguration für Apache, nginx, Netlify und Cloudflare und das, was im Versuch tatsächlich ankam.

Ein falsch gesetzter Header kann mehr kaputt machen als ein fehlender. Ein HSTS-Header mit einem Jahr Laufzeit sperrt Besucher aus, sobald HTTPS irgendwo ausfällt, und der alte X-XSS-Protection-Header kann laut MDN in sonst sicheren Seiten Lücken erst schaffen. Die Note eines Scanners ist deshalb nicht das Ziel. Das Ziel ist ein Satz Header, der passt und nachgemessen ist.

Nachgestellt statt abgeschrieben: Jede Wirkung, die hier behauptet wird, ist im Browser und im Webserver ausprobiert worden, 35 Versuche in Chrome 152 und 12 mit nginx 1.30.5 am 17.09.2026, jeder Lauf zweimal mit demselben Ergebnis. Die Rohdaten liegen zum Nachlesen bereit, die Befunde stehen unter „Die Versuchsreihe“. Dazu kommt eine zweite Messung vom 03.10.2026: welche Header 131 Tiroler Startseiten wirklich mitschicken.

Prüf zuerst, was heute schon ankommt#

Bevor du irgendetwas konfigurierst, sieh dir an, was dein Server heute mitschickt. In der Tiroler Messung unten fehlen bei 48 von 131 Startseiten alle sechs. Der Security-Scan liest die Kopfzeilen einer öffentlichen Adresse aus und sagt bei jedem Header, ob er fehlt, gesetzt ist oder einen Wert trägt, der nichts bewirkt. Drei Wege ohne Werkzeug (curl, die Entwicklerwerkzeuge von Chrome und der Cache-Fallstrick dabei) stehen weiter unten unter Nachprüfen.

Die Header im Überblick#

HeaderStartwertRisiko beim SetzenQuelle
Strict-Transport-Securitymax-age=300; includeSubDomains, danach schrittweise steigernOhne funktionierendes HTTPS ist die Seite für die Dauer von max-age nicht erreichbarhstspreload.org, Cloudflare
Content-Security-Policyzuerst als Content-Security-Policy-Report-Only, etwa default-src 'self'Blockiert eigene Skripte, Schriften und Messdienste, wenn eine Quelle fehltMDN
X-Content-Type-OptionsnosniffStylesheets ohne text/css und Skripte ohne JavaScript-MIME-Typ werden blockiertMDN, OWASP
X-Frame-OptionsSAMEORIGIN (OWASP: DENY)DENY verhindert auch das Einbetten auf der eigenen DomainMDN, OWASP
Referrer-Policystrict-origin-when-cross-originGering, entspricht dem Browser-Standard seit der Spezifikation vom November 2020MDN, OWASP
Permissions-Policygeolocation=(), camera=(), microphone=()Schaltet die Funktion auch für die eigene Seite und alle iframes ab; nicht in allen verbreiteten Browsern unterstütztMDN, OWASP

X-XSS-Protection fehlt in der Tabelle mit Absicht. Warum, steht unter „Was nicht hilft“.

X-Frame-Options und die CSP-Anweisung frame-ancestors tun dasselbe. MDN beschreibt frame-ancestors 'none' als Entsprechung zu X-Frame-Options: deny, wobei der ältere Header auch in alten Browsern greift. Beide zu setzen schadet nicht, solange sie sich nicht widersprechen.

Wie verbreitet das ist: Laut Web Almanac 2025 des HTTP Archive schicken 36 % aller mobil aufgerufenen Seiten einen HSTS-Header, eine Permissions-Policy nur 3,7 % auf Mobilgeräten. Wer die zweite fehlen lässt, ist in großer Gesellschaft. Ein Grund ist das nicht.

Illustration: Sechs flache Karten mit Schild auf einem Server; die erste leuchtet stark grün, nach hinten werden sie blasser.

Die Versuchsreihe: was im Browser wirklich ankommt#

Aufbau: Ein kleiner Server liefert Testseiten auf zwei Origins aus, 127.0.0.1:8101 als eigene Seite und 127.0.0.1:8102 als fremde. Chrome 152 ohne Oberfläche ruft sie auf, und ein Skript liest ab, was passiert ist: Lädt der Rahmen? Läuft das Skript? Was steht beim fremden Server im Referer? Zu jeder Gruppe gibt es einen Kontrollversuch ohne Header.

Einbetten verhindern: X-Frame-Options und frame-ancestors#

Die fremde Origin versucht, die Seite in einem iframe zu laden.

VersuchErgebnis
ohne Header (Kontrolle)lädt
X-Frame-Options: DENY als Headerblockiert
X-Frame-Options: SAMEORIGIN, fremde Origin bettet einblockiert
X-Frame-Options: SAMEORIGIN, eigene Origin bettet einlädt
X-Frame-Options: DENY als <meta http-equiv>lädt, keine Wirkung
Content-Security-Policy: frame-ancestors 'none' als Headerblockiert
dieselbe Anweisung als <meta http-equiv>lädt, keine Wirkung
frame-ancestors 'none' nur als Content-Security-Policy-Report-Onlylädt
X-Frame-Options: DENY und frame-ancestors erlaubt die fremde Originlädt, CSP gewinnt
X-Frame-Options: ALLOW-FROM mit der fremden Originlädt
X-Frame-Options: ALLOW-FROM http://example.com, eine andere Origin bettet einlädt, kein Schutz

Zwei Befunde darin sind in der Praxis teuer. Erstens: Widersprechen sich die beiden Header, gilt in Chrome frame-ancestors. Wer X-Frame-Options: DENY setzt und in der CSP versehentlich eine Origin erlaubt, ist nicht doppelt geschützt, sondern nach der CSP. Zweitens: ALLOW-FROM schützt gar nicht. Chrome verwirft den Wert, und dann darf jede Origin einbetten, nicht nur die genannte.

nosniff: falscher MIME-Typ#

Versuchohne nosniffmit nosniff
Skript, ausgeliefert als text/plainläuftblockiert
Skript als text/javascript (Kontrolle)nicht geprüftläuft
Stylesheet, ausgeliefert als text/plainblockiertblockiert
Stylesheet als text/css (Kontrolle)nicht geprüftgreift

Beim Skript macht nosniff den Unterschied. Beim Stylesheet nicht: Chrome verwirft ein CSS mit falschem Typ in einer Seite mit <!doctype html> schon ohne den Header. Die Folgerung bleibt dieselbe wie bei MDN: nach dem Setzen von nosniff zuerst prüfen, ob die eigenen Skripte mit dem richtigen Typ ausgeliefert werden.

Content-Security-Policy: Inline-Skript#

VersuchInline-Skript
ohne CSP (Kontrolle)läuft
script-src 'self' als Headerblockiert
dieselbe Policy als <meta http-equiv>blockiert
dieselbe Policy als Content-Security-Policy-Report-Onlyläuft, zwei Verletzungen gemeldet
Report-Only als <meta http-equiv>läuft, nichts gemeldet
zwei CSP-Header, der erste erlaubt 'unsafe-inline', der zweite nichtblockiert
ein Header, beide Policies mit Komma verbundenblockiert
Header erlaubt 'unsafe-inline', <meta> verbietet esblockiert

Mehrere Policies addieren sich, und die strengste gewinnt. Eine zweite, lockerere Policy macht eine strenge nicht auf. Das betrifft Hoster, die doppelte Werte zusammenfassen, wie Netlify bei _headers, und Plugins, die zusätzlich eine eigene CSP ins HTML schreiben. Und der Berichtsmodus funktioniert nur als Header: Als <meta> meldet er nichts.

Referrer-Policy#

Die eigene Seite ist mit ?geheim=kundennummer-4711 aufgerufen und schickt eine Anfrage an die fremde Origin.

VersuchWas die fremde Origin im Referer sieht
ohne Policynur http://127.0.0.1:8101/
Referrer-Policy: no-referrer als Headernichts
<meta name="referrer" content="no-referrer">nichts
Referrer-Policy: unsafe-urldie volle Adresse samt ?geheim=kundennummer-4711

Ohne Angabe verhält sich Chrome also schon wie strict-origin-when-cross-origin. Der Header lohnt sich trotzdem, weil er den Standard festschreibt. Gefährlich ist unsafe-url: Damit gehen Parameter aus der Adresse an jeden eingebundenen Dienst.

Permissions-Policy und X-XSS-Protection#

VersuchErgebnis
Geolocation ohne Policy (Kontrolle)erlaubt
Permissions-Policy: geolocation=() als Headergesperrt
dieselbe Policy als <meta http-equiv>erlaubt, keine Wirkung
X-XSS-Protection: 1; mode=block, eingeschleustes Skript steht in Adresse und Seiteläuft

X-XSS-Protection ist in Chrome wirkungslos. Wer sich darauf verlässt, hat keinen Schutz.

Was die Versuchsreihe nicht abdeckt#

Getestet ist Chrome. Firefox und Safari sind nicht nachgestellt. HSTS ist nicht im Versuch, weil sich das Verhalten nur mit gültigem Zertifikat auf einer echten Domain sauber zeigen lässt; dafür steht unten das Beispiel cyberscale.io. Apache, Netlify und Cloudflare sind nach Herstellerdokumentation beschrieben, nicht nachgestellt; nginx ist nachgestellt, die Ergebnisse stehen im nginx-Abschnitt.

HSTS: der Header, den man nicht schnell zurücknimmt#

Der Strict-Transport-Security-Header sagt dem Browser, dass eine Domain für eine bestimmte Zeit nur noch über HTTPS aufgerufen wird. Tippt jemand danach http:// ein, baut der Browser die unverschlüsselte Verbindung gar nicht erst auf.

MDN beschreibt den Fall, für den das gedacht ist: Jemand hat die Seite einmal zu Hause besucht und den Header bekommen. Später im Flughafen-WLAN versucht ein Angreifer, die Verbindung mit einem falschen Zertifikat abzufangen. Weil die Domain als HSTS-Host bekannt ist, lehnt der Browser das Zertifikat ab und lässt kein Wegklicken der Warnung zu.

Drei Anweisungen gibt es:

  • max-age: die Zeit in Sekunden, die sich der Browser die Regel merkt. 31536000 ist ein Jahr.
  • includeSubDomains: die Regel gilt auch für alle Subdomains.
  • preload: der Antrag auf Aufnahme in die Preload-Liste der Browser. Laut MDN verlangt das max-age von mindestens 31536000 und includeSubDomains.

Zwei Regeln aus RFC 6797 erklären, warum HSTS sich schlecht rückgängig machen lässt. Ein über unverschlüsseltes HTTP empfangener Header wird ignoriert (Abschnitt 8.1). Und max-age=0 hebt die Regel zwar auf (Abschnitt 6.1.1), aber laut MDN erst, wenn der Browser wieder eine sichere Anfrage stellt und die neue Antwort bekommt. Wer HTTPS abschaltet, kann den Browsern also nicht mehr mitteilen, dass HSTS vorbei ist. Cloudflare formuliert die Folge ausdrücklich: Wird HTTPS entfernt, bevor die ursprüngliche Laufzeit abgelaufen ist, ist die Website für diese Laufzeit nicht erreichbar.

Preload: die Lücke beim ersten Besuch schließen#

Ohne Preload ist der allererste Aufruf über http:// noch ungeschützt, denn der Browser kennt die Regel ja noch nicht. Die Preload-Liste auf hstspreload.org, die laut MDN alle großen Browser nutzen, schließt diese Lücke: Die Domain ist dem Browser schon ab Werk bekannt.

Die Voraussetzungen laut hstspreload.org:

  1. ein gültiges Zertifikat,
  2. Weiterleitung von HTTP auf HTTPS auf demselben Host, falls Port 80 bedient wird,
  3. alle Subdomains über HTTPS,
  4. ein HSTS-Header auf der Hauptdomain mit max-age von mindestens 31536000, includeSubDomains und preload.

Punkt 3 ist der kritische. hstspreload.org weist ausdrücklich darauf hin, dass dazu auch interne Subdomains gehören, die öffentlich gar nicht erreichbar sind. Das Zurücknehmen ist möglich, dauert aber: Laut hstspreload.org braucht eine Streichung Monate, bis sie mit einem Chrome-Update bei den Nutzern ankommt, und für andere Browser gibt es keine Zusage. Die Seite nennt das Entfernen wörtlich „slow and painful“.

Der Weg, in Stufen#

hstspreload.org empfiehlt, die Laufzeit in Stufen hochzuziehen und auf jeder Stufe die volle max-age-Dauer abzuwarten, bevor die nächste kommt. Der Weg dauert damit gut fünf Wochen.

  1. max-age=300; includeSubDomains: fünf Minuten. Alle Subdomains durchklicken, auch die internen.
  2. max-age=604800; includeSubDomains: eine Woche. Auf kaputte Seiten und Fehlermeldungen achten.
  3. max-age=2592000; includeSubDomains: ein Monat.
  4. max-age=31536000; includeSubDomains: ein Jahr. Das ist die Mindestlaufzeit für die Preload-Liste.
  5. Nur wenn jede Subdomain dauerhaft HTTPS kann: preload ergänzen und über das Formular auf hstspreload.org einreichen. OWASP empfiehlt als Wert max-age=63072000; includeSubDomains; preload, also zwei Jahre.

Wer bei Stufe 1 nicht alle Subdomains kennt, gehört nicht zu Stufe 5.

HSTS in fünf Stufen hochziehen Zeitleiste mit fünf Stationen für den Wert max-age: 300 Sekunden, also fünf Minuten; 604800, eine Woche; 2592000, ein Monat; 31536000, ein Jahr; danach preload und die Einreichung bei hstspreload.org. Auf jeder Stufe wird die volle Laufzeit abgewartet, bevor die nächste kommt. max-age=300 604800 2592000 31536000 + preload 5 Minuten 1 Woche 1 Monat 1 Jahr Liste Subdomains prüfen Fehler suchen abwarten Mindestwert hstspreload.org Jede Stufe die volle Laufzeit stehen lassen, immer mit includeSubDomains. Stufen nach hstspreload.org; Mindestwert für preload laut MDN.
Der Weg zu HSTS mit Preload: vier Laufzeiten, jede voll abgewartet, erst dann die Einreichung. Zusammen gut fünf Wochen. Stufen und Voraussetzungen nach hstspreload.org, Mindestlaufzeit für preload laut MDN (Strict-Transport-Security). Schema, keine Messung.

Konfiguration zum Kopieren#

Die Beispiele setzen den HSTS-Header mit der ersten Stufe und die Content-Security-Policy im Berichtsmodus. Beides ist Absicht. Scharf gestellt wird erst, wenn die Messung sauber ist; wie der Berichtsmodus ausgewertet wird, steht in Content-Security-Policy-Report-Only.

Apache (.htaccess oder Serverkonfiguration)#

Header always set Strict-Transport-Security "max-age=300; includeSubDomains"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), camera=(), microphone=()"
Header always set Content-Security-Policy-Report-Only "default-src 'self'"

Drei Punkte aus der Apache-Dokumentation zu mod_headers:

  • set ersetzt einen vorhandenen Header gleichen Namens, add hängt einen zweiten daneben. Doppelte Security-Header will niemand, deshalb set.
  • always setzt den Header auch auf Fehlerantworten und bei internen Weiterleitungen wie ErrorDocument. Ohne always gilt die Standardbedingung onsuccess.
  • In der .htaccess funktioniert die Direktive nur, wenn AllowOverride für diesen Ordner FileInfo erlaubt.

Dass der HSTS-Header hier auch auf HTTP-Antworten landet, schadet nicht: Browser ignorieren ihn dort nach RFC 6797. In einer WordPress-.htaccess gehören die Zeilen außerhalb des Blocks # BEGIN WordPress … # END WordPress, den WordPress bei Änderungen an den Permalinks neu schreibt.

nginx#

# im server-Block, der HTTPS ausliefert
add_header Strict-Transport-Security "max-age=300; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
add_header Content-Security-Policy-Report-Only "default-src 'self'" always;

location /downloads/ {
    add_header Cache-Control "no-store";
    # Achtung: hier fehlen jetzt alle sechs Header von oben
}

Hier liegt die Falle bei nginx. Die nginx-Dokumentation sagt: add_header-Direktiven werden von der übergeordneten Ebene nur dann geerbt, wenn auf der aktuellen Ebene keine eigene add_header-Direktive steht. Ein einziges add_header in einem location-Block löscht dort also alle Security-Header des server-Blocks. Die Lösung ist, sie im location-Block zu wiederholen. Ab nginx 1.29.3 gibt es außerdem add_header_inherit merge;, das die Werte der übergeordneten Ebene an die eigenen anhängt.

Ohne always setzt nginx die Header nur bei den Statuscodes 200, 201, 204, 206, 301, 302, 303, 304, 307 und 308. Eine 404-Seite bliebe ohne.

Nachgestellt mit nginx 1.30.5 (offizielle Windows-Ausgabe von nginx.org, Signatur geprüft). Gezählt ist, wie viele der Security-Header in der Antwort ankommen:

AufbauAnfrageSecurity-Header
ohne alwaysnormale Seite, 2006 von 6
ohne alwaysWeiterleitung, 3016 von 6
ohne alwaysnicht vorhandene Seite, 4040 von 6
ohne alwaysServerfehler, 5000 von 6
mit always404 und 5006 von 6
mit always, location ohne eigenes add_header2006 von 6
mit always, location setzt nur Cache-Control2000 von 6
dasselbe mit add_header_inherit merge;2006 von 6, dazu Cache-Control
zwei Header im http-Block, server-Block ohne add_header2002 von 2
zwei Header im http-Block, server-Block setzt einen eigenen Header2000 von 2

Die letzte Zeile ist dieselbe Falle eine Ebene höher: Wer die Security-Header zentral in den http-Block schreibt und in einem server-Block irgendeinen anderen Header ergänzt, hat dort keine Security-Header mehr.

Netlify#

Entweder eine Datei _headers im Veröffentlichungsordner:

/*
  Strict-Transport-Security: max-age=300; includeSubDomains
  X-Content-Type-Options: nosniff
  X-Frame-Options: SAMEORIGIN
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: geolocation=(), camera=(), microphone=()
  Content-Security-Policy-Report-Only: default-src 'self'

oder derselbe Inhalt in der netlify.toml:

[[headers]]
  for = "/*"
  [headers.values]
    Strict-Transport-Security = "max-age=300; includeSubDomains"
    X-Content-Type-Options = "nosniff"
    X-Frame-Options = "SAMEORIGIN"
    Referrer-Policy = "strict-origin-when-cross-origin"
    Permissions-Policy = "geolocation=(), camera=(), microphone=()"
    Content-Security-Policy-Report-Only = "default-src 'self'"

Die Einschränkung steht in der Netlify-Dokumentation: Eigene Header gelten nur für Dateien, die Netlify selbst ausliefert. Für weitergeleitete Inhalte per Proxy, Functions und Edge Functions, also auch serverseitig gerenderte Seiten, muss die Funktion oder das Ziel die Header selbst mitschicken. Steht derselbe Header in _headers mehrfach unter einem Pfad, fügt Netlify die Werte zu einem Header zusammen.

Cloudflare#

Bei Cloudflare liegen die Header an zwei Stellen:

  1. HSTS im Dashboard auf der Seite Edge Certificates. Dort gibt es Schalter für max-age (ein bis zwölf Monate), „Apply to Subdomains“, „Preload“ und zusätzlich „No-Sniff Header“ für X-Content-Type-Options: nosniff.
  2. Alle anderen als Response Header Transform Rule: im Dashboard Rules → Overview → Create rule → Response Header Transform Rule, als Aktion Set static, dann Header-Name und Wert eintragen. Eine Regel kann bis zu 30 Header setzen.

Laut Cloudflare gelten diese Regeln auch für Cloudflares eigene Fehlerseiten. Den server-Header und Header, die mit cf- beginnen, lässt Cloudflare nicht ändern. Die Liste der Aktionen, die nach aktivem HSTS nicht mehr passieren dürfen, ist deutlich: DNS von „Proxied“ auf „DNS only“ umstellen, Cloudflare pausieren, HTTPS auf HTTP umleiten, Zertifikate abschalten.

WordPress#

Die offiziellen Härtungs- und HTTPS-Seiten auf developer.wordpress.org beschreiben keinen Weg dafür. Der belegbare Weg ist deshalb die Serverebene: bei Apache die Zeilen oben in die .htaccess oder den virtuellen Host, bei nginx in den server-Block, bei einem vorgeschalteten CDN dort. Plugins, die Header setzen, gibt es; eine Herstellerdokumentation, auf die sich eine Empfehlung stützen ließe, gibt es für sie nicht.

Illustration: Ein Browserfenster als Umriss auf einem Server; die grünen Header-Karten gleiten unten aus dem Server heraus, nicht aus dem Fenster.

Das Beispiel: cyberscale.io, gemessen am 15.09.2026#

In eigener Sache: Das Beispiel ist diese Seite selbst, gehostet bei Netlify, am 03.10.2026 nachgemessen und unverändert. Die Antwort auf https://www.cyberscale.io/, die Content-Security-Policy gekürzt:

HTTP/1.1 200
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net …; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requests
Permissions-Policy: geolocation=(), camera=(), microphone=()
Referrer-Policy: strict-origin-when-cross-origin
Server: Netlify
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN

Die Weiterleitungskette: http://cyberscale.io/ → 301 → https://cyberscale.io/ → 301 → https://www.cyberscale.io/. Die Verbindung läuft über TLS 1.3.

Was stimmt:

  • HSTS mit einem Jahr und includeSubDomains, und zwar schon auf der Antwort der Hauptdomain ohne www. Das ist Voraussetzung 4 der Preload-Liste bis auf das fehlende preload.
  • Die erste Weiterleitung bleibt auf demselben Host und wechselt nur das Protokoll. Genau das verlangt Voraussetzung 2; ein Sprung von http://cyberscale.io/ direkt auf https://www. erfüllte sie nicht.
  • nosniff, SAMEORIGIN und frame-ancestors 'self' sagen dasselbe und widersprechen sich nicht.
  • Permissions-Policy schaltet Standort, Kamera und Mikrofon ab. Die Seite braucht keine der drei.
  • Die CSP enthält object-src 'none', base-uri 'none', form-action 'self' und upgrade-insecure-requests.

Was offen ist:

  • 'unsafe-inline' in script-src. Damit erlaubt die Policy eingebettete Skripte, und genau die sind der Weg, auf dem eingeschleuster Code läuft. MDN nennt als starke Policy eine, die Inline-JavaScript abschaltet. Wie man dahin kommt, steht im CSP-Leitfaden.
  • Kein preload. Die Laufzeit reicht, aber die Aufnahme setzt voraus, dass jede Subdomain, auch jede künftige, dauerhaft HTTPS kann. Diese Entscheidung ist nicht mit einer Zeile Konfiguration getroffen.
  • Server: Netlify verrät den Hoster. OWASP empfiehlt einen nichtssagenden Wert. Das nimmt Angreifern eine Information, schließt aber keine Lücke.

Der Security-Scan kommt am 03.10.2026 für dieselbe Adresse zum selben Befund: 14 Prüfungen bestanden, eine Warnung für das 'unsafe-inline', drei Hinweise (Server-Header, DNSSEC, eine fremde Skript-Quelle).

Security-Scan für www.cyberscale.io: Score 95, 0 kritisch, 1 Warnung, 3 Hinweise, 14 bestanden. Warnung: Content-Security-Policy stark abgeschwächt, die Policy erlaubt 'unsafe-inline' für Skripte. Hinweise: Server-Header nennt Netlify, DNSSEC nicht eingerichtet, 1 fremde Skript-Quelle cloud.umami.is. Bestanden: HTTPS, HSTS 365 Tage, X-Content-Type-Options, X-Frame-Options SAMEORIGIN, Referrer-Policy, Permissions-Policy, keine riskanten Ports, Zertifikat von Let's Encrypt gültig bis 20.11.2026, TLS 1.3, SPF, DMARC p=quarantine, Zertifikatskette.
Der Security-Scan für www.cyberscale.io: Score 95, eine Warnung wegen 'unsafe-inline' in der Content-Security-Policy, drei Hinweise, 14 Prüfungen bestanden. Zum Werkzeug · Screenshot vom 03.10.2026, unbearbeitet.

Wie es draußen aussieht: 131 Tiroler Websites#

In eigener Sache eine zweite Messung, diesmal nicht im Labor: Am 03.10.2026 habe ich die Startseiten von 140 Tiroler Domains einmal per curl abgerufen, so wie ein Browser es tut, und die Antwort-Header gelesen. 131 antworteten mit 200. Die Domains stammen aus meiner Akquise-Liste, 115 Betriebe (Hotels, Praxen, Kanzleien, Handwerk, Agenturen, Stadtwerke) und 25 Tourismusverbände. Gezählt ist, ob der Header vorhanden ist, bei HSTS zusätzlich der Wert. Nur Summen, keine Namen.

Security-Header auf 131 Tiroler Startseiten, 03.10.2026 Balken je Header, Anzahl der Startseiten mit diesem Header: X-Content-Type-Options 61, Strict-Transport-Security 45, Einbettungsschutz per X-Frame-Options oder frame-ancestors 42, Referrer-Policy 31, Content-Security-Policy 16, Permissions-Policy 11. 48 Startseiten senden keinen einzigen der sechs Header. X-Content-Type-Options Strict-Transport-Security Einbettungsschutz Referrer-Policy Content-Security-Policy Permissions-Policy keiner der sechs 61 · 47 % 45 · 34 % 42 · 32 % 31 · 24 % 16 · 12 % 11 · 8 % 48 · 37 % 131 von 140 Startseiten erreichbar, Header per curl, 03.10.2026.
Welche Security-Header 131 Tiroler Startseiten mitschicken: am häufigsten nosniff, am seltensten die Permissions-Policy. 48 senden keinen einzigen. Eigene Messung: Antwort-Header der Startseite per curl (GET, wie ein Browser), 140 Domains, 131 erreichbar, 03.10.2026 04:07 UTC. Nur Summen.
HeadergesetztAnteil
X-Content-Type-Options61 von 13147 %
Strict-Transport-Security4534 %
Einbettungsschutz (X-Frame-Options oder frame-ancestors)4232 %
Referrer-Policy3124 %
Content-Security-Policy1612 %
Permissions-Policy118 %
keiner der sechs4837 %
alle sechs22 %

Das passt zum Web Almanac: HSTS bei einem guten Drittel, die Permissions-Policy fast nirgends. Die Details sagen mehr als die Summen.

  • HSTS: 32 der 45 Einträge haben ein max-age von mindestens einem Jahr, 13 sind kürzer, darunter 1800 und 3600 Sekunden, die praktisch nichts festschreiben. 15 tragen includeSubDomains, 15 preload. Keiner steht auf max-age=0.
  • CSP: Von 16 Policies sind 11 reine frame-ancestors-Anweisungen, also Einbettungsschutz ohne Skriptregel. Nur 4 enthalten script-src oder default-src, 3 davon mit 'unsafe-inline', eine mit Nonce. Eine Policy besteht nur aus upgrade-insecure-requests, eine erlaubt mit frame-ancestors * jedem das Einbetten. Drei Sites senden zusätzlich Content-Security-Policy-Report-Only, keine einzige Reporting-Endpoints.
  • X-Frame-Options: 36-mal SAMEORIGIN, zweimal DENY, einmal ein ALLOW-FROM, das Chrome verwirft (siehe Versuchsreihe), und einmal ein Wert, der in keiner Spezifikation steht. Die letzten beiden Sites halten sich für geschützt und sind es nicht.
  • Referrer-Policy: 6 der 31 Antworten tragen zwei Werte durch Komma getrennt, etwa no-referrer-when-downgrade, strict-origin-when-cross-origin. Mehrere Werte sind im Header erlaubt, der Browser nimmt den letzten, den er kennt. Es zeigt aber, dass zwei Stellen denselben Header setzen, und die zweite weiß nichts von der ersten.
  • Server: 11 Antworten nennen die Version der Serversoftware, darunter dreimal Apache/2.2.22, eine Reihe, die das Apache-Projekt als „end-of-life“ führt. 17 senden X-Powered-By: PHP/…, 6 davon mit einer PHP-Version ohne Sicherheitskorrekturen (5.6, 7.2, 7.4, 8.1).

Der häufigste Zustand ist also nicht der falsch gesetzte Header, sondern gar keiner: 48 von 131. Für sie gelten die drei Zeilen ohne Wartezeit aus der Tabelle oben.

Nachprüfen#

Nicht annehmen, dass ankommt, was in der Konfiguration steht. Drei Wege:

1. Mit curl:

curl -sI https://www.example.com/
curl -sIL http://example.com/

-I holt laut curl-Handbuch nur die Kopfzeilen per HEAD, -s unterdrückt die Fortschrittsanzeige, -L folgt Weiterleitungen. Der zweite Aufruf zeigt die ganze Kette von der unverschlüsselten Hauptdomain aus, samt der Header jeder Zwischenstation. Auch eine Unterseite und eine nicht existierende Adresse abfragen: Dort fallen fehlendes always und die nginx-Vererbung auf. 2. In den Entwicklerwerkzeugen von Chrome: Reiter Network, Disable cache anhaken, Seite neu laden, die Anfrage auswählen, Reiter Headers, Abschnitt Response Headers. 3. Mit dem Security-Scan: Er prüft HSTS, Content-Security-Policy, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy sowie Server und X-Powered-By. Fünf davon liest auch der E-Mail-Check, zusammen mit SPF, DKIM und DMARC der Domain.

Bevor ein Ergebnis zählt, muss der Cache aus dem Weg sein. Sonst sind die Header gesetzt, aber ein CDN oder der Browser liefert noch die alte Antwort. Deshalb nach jeder Änderung den Cache des Hosters oder CDN leeren und im Browser „Disable cache“ nutzen, bevor das Ergebnis zählt.

Was Security-Header mit SEO zu tun haben#

Direkt nichts. Google schreibt in der Dokumentation zur Seitenerfahrung, dass die Core Web Vitals von den Ranking-Systemen genutzt werden, andere Aspekte der Seitenerfahrung aber nicht direkt zu besseren Positionen verhelfen. Security-Header erwähnt die Seite nicht; HTTPS steht dort als Frage zur Selbstprüfung.

Indirekt zählen zwei Dinge. Erstens gemischte Inhalte: Laut MDN heben Browser unverschlüsselt eingebundene Bilder, Audio und Video selbst auf HTTPS, blockieren aber Skripte, Stylesheets, iframes, Webfonts und fetch()-Anfragen. Google rendert Seiten laut eigener Dokumentation mit einer aktuellen Chromium-Version. Was im Browser fehlt, kann auch beim Rendern fehlen. Die CSP-Anweisung upgrade-insecure-requests hebt solche Aufrufe auf HTTPS, auch die sonst blockierten.

Zweitens die Content-Security-Policy selbst. Eine scharf gestellte Policy, die ein eigenes Skript nicht zulässt, blockiert es. Ob Googles Renderer eine CSP durchsetzt, sagt die Google-Dokumentation nicht. Wer Inhalte per JavaScript nachlädt, sollte es nicht darauf ankommen lassen und die Policy zuerst im Berichtsmodus laufen lassen.

Was per Meta-Tag und mit alten Headern nicht geht#

X-XSS-Protection: 1; mode=block ist überholt. MDN rät von dem Header in neuen Projekten ab und warnt, dass er in manchen Fällen XSS-Lücken in sonst sicheren Seiten erzeugt. Die Empfehlung ist eine Content-Security-Policy. OWASP empfiehlt X-XSS-Protection: 0, also den Filter ausdrücklich abzuschalten. Im Versuch lief ein eingeschleustes Skript trotz 1; mode=block in Chrome 152 ungehindert.

Header per <meta http-equiv> zu setzen funktioniert nur für einen Teil:

  • HSTS per <meta> darf ein Browser nach RFC 6797, Abschnitt 8.5, nicht beachten.
  • X-Frame-Options in einem <meta>-Element hat laut MDN keine Wirkung.
  • Content-Security-Policy per <meta> geht, aber ohne frame-ancestors, report-uri, report-to und sandbox. Schutz vor Einbettung und Verletzungsberichte gibt es so nicht. Im Versuch blockierte die <meta>-Policy das Inline-Skript, ließ die Einbettung aber zu, und der Berichtsmodus als <meta> meldete nichts.
  • Permissions-Policy per <meta> hatte im Versuch keine Wirkung; Geolocation blieb erlaubt.
  • Referrer-Policy geht als <meta name="referrer">, die einzige der sechs, die im HTML vollständig funktioniert.

X-Frame-Options: ALLOW-FROM ist laut MDN veraltet; moderne Browser ignorieren Antworten mit dieser Anweisung. Im Versuch hieß das: Auch eine Origin, die gar nicht genannt war, durfte einbetten. Wer bestimmte fremde Seiten zum Einbetten zulassen will, nimmt frame-ancestors.

preload gleich am ersten Tag zu setzen geht schief. Die Stufen auf hstspreload.org gibt es, weil erst die kurzen Laufzeiten zeigen, welche Subdomain kein HTTPS kann. Mit preload zeigt es sich erst, wenn die Liste schon ausgeliefert ist.

Und den Server-Namen zu verstecken ist keine Sicherheit. OWASP empfiehlt es, und es ist in Ordnung; eine veraltete Software wird davon trotzdem nicht aktueller.

Der Spickzettel als PDF#

Die sechs Header mit Startwert, die HSTS-Stufen, die Zeilen für Apache, nginx und Netlify und die Fallen aus der Versuchsreihe stehen auf einer DIN-A4-Seite: Security-Header-Spickzettel herunterladen (PDF). Ohne Anmeldung und ohne E-Mail-Adresse. Der Stand steht auf dem Zettel; ändert sich an den Werten etwas, ändern sich Guide und Zettel gemeinsam.

Häufige Fragen#

Reicht es, die Header per <meta>-Tag ins HTML zu schreiben?

Nur für einen Teil. Im Versuch wirkten script-src und die Referrer-Policy per <meta>; X-Frame-Options, frame-ancestors, Permissions-Policy und der Berichtsmodus der CSP wirkten nicht. HSTS per <meta> darf ein Browser laut RFC 6797 gar nicht beachten.

Was passiert, wenn X-Frame-Options und frame-ancestors sich widersprechen?

Chrome folgt frame-ancestors. Im Versuch lud die Seite in einem fremden Rahmen, obwohl X-Frame-Options: DENY gesetzt war, weil die CSP die fremde Origin erlaubte.

Brauche ich X-Frame-Options noch, wenn frame-ancestors gesetzt ist?

In Chrome 152 hatte frame-ancestors Vorrang; andere Browser sind hier nicht nachgestellt. Laut MDN greift der ältere Header auch in alten Browsern. Beide zu setzen schadet nicht, solange sie dasselbe sagen.

Mein Hoster setzt schon eine CSP, und ich setze eine zweite. Welche gilt?

Beide. Der Browser prüft jede Policy für sich und blockiert, was eine davon verbietet. Eine zweite, lockerere Policy macht eine strengere nicht auf; im Versuch blieb das Inline-Skript in allen drei Varianten blockiert.

Warum fehlen bei nginx die Header auf der 404-Seite?

Weil add_header ohne always nur bei bestimmten Statuscodes greift, und 404 und 500 gehören nicht dazu. Im Versuch kamen dort 0 von 6 Headern an, mit always alle sechs.

Verbessern Security-Header das Ranking?

Nicht direkt. Google nennt außer den Core Web Vitals keine Aspekte der Seitenerfahrung, die zu besseren Positionen verhelfen. Indirekt zählt, dass eine falsch gesetzte CSP eigene Skripte blockieren kann, die Inhalte nachladen.

Drei Zeilen sofort, HSTS in Stufen, CSP im Berichtsmodus#

Die drei harmlosen Header (X-Content-Type-Options, X-Frame-Options, Referrer-Policy) sind je eine Zeile ohne Wartezeit. Die Permissions-Policy auch, solange die Seite keine Kamera, kein Mikrofon und keinen Standort braucht. HSTS braucht fünf Wochen Geduld und eine vollständige Liste der Subdomains, und die Content-Security-Policy braucht den Berichtsmodus, weil erst er zeigt, welche Quellen die Seite wirklich lädt.

Wer die Header setzt, weil eine Seite schon einmal manipuliert wurde, sollte vorher prüfen, ob sie es noch ist: Ein Header verhindert neuen Schaden, er entfernt keinen alten. Woran man das erkennt, steht in WordPress gehackt: was tun, im ersten Abschnitt. Und weil HSTS ohne ein sauberes Zertifikat auf jeder Subdomain nicht funktioniert, gehört der Blick auf Zertifikat und Laufzeit jeder Subdomain vor Stufe 1, nicht danach; der Security-Scan prüft beides.

Stand 03.10.2026. Quellen abgerufen, eigene Header nachgemessen und die Tiroler Messung an diesem Tag durchgeführt; die Versuchsreihe stammt vom 17.09.2026.

Quellen (29)
  • MDN, Strict-Transport-Security: „When using preload, the max-age directive must be at least 31536000 (1 year), and the includeSubDomains directive must be present.“ Außerdem: „To disable HSTS, set max-age=0. This only takes effect once the browser makes a secure request“.
  • hstspreload.org, HSTS Preload List Submission: Voraussetzungen „Serve a valid certificate“, „Redirect from HTTP to HTTPS on the same host“, „Serve all subdomains over HTTPS“; Stufen max-age=300, 604800, 2592000; „it takes months for a change to reach users with a Chrome update“.
  • IETF, RFC 6797: HTTP Strict Transport Security: Abschnitt 8.1 „If an HTTP response is received over insecure transport, the UA MUST ignore any present STS header field(s)“; Abschnitt 8.5 „UAs MUST NOT heed http-equiv="Strict-Transport-Security" attribute settings on <meta> elements“; Abschnitt 6.1.1 zu max-age=0.
  • OWASP, HTTP Security Response Headers Cheat Sheet: „X-XSS-Protection: 0“, „Strict-Transport-Security: max-age=63072000; includeSubDomains; preload“, „Permissions-Policy: geolocation=(), camera=(), microphone=()“, „Server: webserver“.
  • MDN, X-XSS-Protection: „in some cases, X-XSS-Protection can create XSS vulnerabilities in otherwise safe websites.“
  • MDN, X-Content-Type-Options: „Blocks a request if the request destination is of type style and the MIME type is not text/css“.
  • MDN, X-Frame-Options: „Setting X-Frame-Options inside the <meta> element … has no effect.“ Zu ALLOW-FROM: veraltet, von modernen Browsern ignoriert.
  • MDN, CSP: frame-ancestors: „This directive is not supported in the <meta> element.“
  • MDN, Content-Security-Policy: frame-ancestors, report-uri, report-to und sandbox nicht per <meta>.
  • MDN, Referrer-Policy: strict-origin-when-cross-origin als Standard ohne Angabe laut Spezifikation vom November 2020; <meta name="referrer">.
  • MDN, Permissions-Policy: „Permissions-Policy: geolocation=()“; Hinweis „not Baseline because it does not work in some of the most widely-used browsers“.
  • MDN, Mixed content: Bilder, Audio und Video werden hochgestuft, Skripte und Stylesheets blockiert; upgrade-insecure-requests.
  • Apache HTTP Server, mod_headers: „the headers contained in the latter are added to the response even on error, and persisted across internal redirects“; Context „.htaccess“, Override „FileInfo“.
  • nginx, ngx_http_headers_module: „These directives are inherited from the previous configuration level if and only if there are no add_header directives defined on the current level.“ add_header_inherit ab 1.29.3.
  • Netlify, Custom headers: „Custom headers apply only to files Netlify serves from our own backing store.“
  • Cloudflare, HTTP Strict Transport Security: „your website becomes inaccessible to visitors for the duration of the Max Age Header.“
  • Cloudflare, Response header transform rules und Anlegen im Dashboard: „Response header transform rules will also apply to default Cloudflare error pages“; Rules → Overview → Create rule.
  • WordPress, Hardening WordPress und HTTPS: keine Angaben zu Security-Headern (geprüft am 15.09.2026).
  • HTTP Archive, Web Almanac 2025, Kapitel Security: „up to 36% of all pages visited on mobile“ (HSTS); „Permissions-Policy remains rather small at 3.7% on mobile“.
  • Google Search Central, Seitenerfahrung: „Beyond Core Web Vitals, other page experience aspects don't directly help your website rank higher.“
  • Google Search Central, JavaScript-SEO-Grundlagen: „Google Search runs JavaScript with an evergreen version of Chromium.“
  • curl, Handbuch: -I „Fetch the headers only“, -L „follow HTTP redirects“.
  • Chrome for Developers, Network features reference: Headers-Reiter, Abschnitt Response Headers, „Disable cache“.
  • W3C, Content Security Policy Level 3: mehrere Policies werden jede für sich durchgesetzt; Content-Security-Policy-Report-Only wird in einem <meta>-Element nicht unterstützt.
  • Eigene Messung von cyberscale.io per curl am 15.09.2026, wiederholt am 03.10.2026 (unverändert).
  • Eigene Messung: Antwort-Header der Startseiten von 140 Tiroler Domains (131 erreichbar) per curl, GET wie ein Browser, am 03.10.2026 04:07 UTC; zweiter Durchgang 04:12 UTC für Content-Security-Policy-Report-Only, Reporting-Endpoints, Report-To und X-Powered-By. Veröffentlicht werden nur Summen.
  • W3C, Referrer Policy, Abschnitt „Parse a referrer policy from a Referrer-Policy header“: bei mehreren Werten gilt der letzte bekannte.
  • Apache HTTP Server Project, Startseite: „Apache httpd 2.2 is end-of-life.“ (abgerufen am 03.10.2026)
  • Eigene Versuchsreihe am 17.09.2026: 35 Versuche in Chrome 152.0.7977.83 ohne Oberfläche, gesteuert über puppeteer-core, und 12 Versuche mit nginx 1.30.5 für Windows, jeweils zwei Läufe mit identischem Ergebnis. Rohdaten als JSON.

Weiterlesen