Security Header sind der Teil der Web-Sicherheit, der am wenigsten kostet und am häufigsten fehlt. Der HTTP Archive Web Almanac 2025 zählt auf 36 Prozent der mobil gecrawlten Seiten einen HSTS-Header und auf 21,9 Prozent eine Content Security Policy. Der Rest setzt keinen der beiden Header.
Dieser Artikel ordnet die sieben relevanten Header ein, nennt die empfohlenen Werte und liefert fertige Konfigurationsblöcke für Nginx, Caddy und die Shopware-.htaccess. Die Zeitangabe im Titel gilt für alles außer der Content Security Policy; die bekommt einen eigenen Abschnitt, weil sie als einziger Header einen laufenden Shop beschädigen kann.
Was Security Header sind und was sie nicht leisten
Ein Security Header ist eine Zeile in der HTTP-Antwort deines Servers, die der Browser als Anweisung liest. Strict-Transport-Security: max-age=63072000 heißt: Sprich mit dieser Domain die nächsten zwei Jahre nur noch über HTTPS, auch wenn jemand http:// tippt oder ein Link ohne s im Umlauf ist. Der Server erzwingt nichts selbst. Er gibt die Regel vor, der Browser setzt sie durch.
Daraus folgt die Grenze: Security Header schützen den Browser des Nutzers, nicht deinen Server. Eine SQL-Injection im Checkout, ein ungepatchtes Plugin oder ein offener Admin-Zugang bleiben, was sie sind. Der Header verkleinert die Angriffsfläche im Client: eingeschleuste Skripte laufen nicht, die Seite lässt sich nicht in einen fremden Frame einbetten, der Browser interpretiert eine hochgeladene Textdatei nicht als JavaScript.
Referenz für die empfohlenen Werte ist das OWASP Secure Headers Project. Es pflegt eine maschinenlesbare Liste der Header, die gesetzt werden sollen, und eine zweite Liste der Header, die weg sollen: Server, X-Powered-By, X-AspNet-Version und rund 85 weitere Kennungen, die Angreifern Software und Version verraten. Die zweite Liste wird in Audits am häufigsten übersehen, obwohl ein Scanner ein Server: nginx/1.18.0 direkt gegen die CVE-Liste dieser Version hält.
Die sieben Header im Überblick
Die Tabelle zeigt die Header, die aktuell zählen. Als Wert steht der OWASP-Vorschlag oder, wo der in einem Shop zu weit geht, die shop-taugliche Variante; die dritte Spalte ordnet ein, wie riskant das Setzen für einen laufenden Shop ist.
| Header | OWASP-Wert bzw. shop-taugliche Variante | Schützt vor | Risiko beim Setzen |
|---|---|---|---|
Strict-Transport-Security |
max-age=63072000; includeSubDomains |
Downgrade auf HTTP, Cookie-Diebstahl im offenen WLAN | Mittel: sperrt HTTP-Subdomains aus, lange Laufzeit |
Content-Security-Policy |
Projektabhängig; Minimum object-src 'none'; base-uri 'self'; frame-ancestors 'self' (OWASP: 'none') |
Cross-Site-Scripting, Clickjacking, Content-Injection | Hoch: blockiert eigene Skripte, Payment und Tracking, wenn falsch |
X-Frame-Options |
DENY (OWASP) oder SAMEORIGIN |
Clickjacking | Gering: nur relevant, wenn der Shop bewusst in Frames läuft |
X-Content-Type-Options |
nosniff |
MIME-Sniffing (Datei als Skript ausgeführt) | Keins |
Referrer-Policy |
strict-origin-when-cross-origin (OWASP: no-referrer) |
Weitergabe interner URLs und Parameter an Dritte | Gering: Affiliate- und Analytics-Attribution prüfen |
Permissions-Policy |
camera=(), microphone=(), geolocation=(), usb=() ... |
Zugriff eingebetteter Inhalte auf Gerätefunktionen | Gering: payment=() nur setzen, wenn kein Payment-Request-API-Checkout läuft |
Cross-Origin-Opener-Policy |
same-origin-allow-popups (OWASP: same-origin) |
Zugriff fremder Fenster auf dein window-Objekt |
Gering mit allow-popups; same-origin bricht Popup-Checkouts wie PayPal |
Drei Header aus älteren Anleitungen gehören nicht mehr in die Konfiguration. X-XSS-Protection steuerte einen Browser-Filter, den Chrome 2019 entfernt hat und den Firefox nie hatte; die MDN-Dokumentation rät explizit davon ab, weil der Filter in bestimmten Fällen selbst Lücken öffnet. Expect-CT ist seit dem Ende der Certificate-Transparency-Übergangsphase funktionslos. Public-Key-Pins (HPKP) hat Chrome 2019 und Firefox 2020 wegen des Aussperr-Risikos entfernt, Safari hat es nie unterstützt. Wer diese drei noch setzt, schadet nicht, zeigt aber, dass die Konfiguration seit Jahren niemand angefasst hat.
Cross-Origin-Embedder-Policy: require-corp steht ebenfalls in der OWASP-Liste, gehört aber nicht in einen Shop. Der Header verlangt, dass jede Cross-Origin-Ressource per CORP oder CORS ausdrücklich freigegeben ist, und erzeugt zusammen mit COOP eine sogenannte cross-origin-isolierte Seite. Stripe schreibt in seinem Security Guide, dass Stripe solche Seiten nicht unterstützt, weil zentrale Abhängigkeiten der Zahlungsabwicklung das nicht tun. Dasselbe gilt für die meisten Payment- und Tracking-Einbindungen.
HSTS richtig setzen: max-age, includeSubDomains, preload
HSTS (HTTP Strict Transport Security) ist der Header mit dem besten Verhältnis aus Aufwand und Wirkung. Ohne HSTS schickt der Browser beim ersten Aufruf von http://deinshop.de einen unverschlüsselten Request, bevor dein Server auf HTTPS umleitet. In diesem einen Request lassen sich im offenen WLAN Session-Cookies abgreifen oder der Nutzer auf eine Kopie der Seite lenken. Mit HSTS merkt sich der Browser nach dem ersten HTTPS-Besuch die Regel und schreibt jeden späteren http://-Aufruf lokal auf https:// um, bevor ein Byte das Gerät verlässt.
Der Header hat drei Bestandteile:
max-agein Sekunden. OWASP empfiehlt 63072000 (zwei Jahre), die HSTS-Preload-Liste verlangt mindestens 31536000 (ein Jahr). Der Browser merkt sich die Regel genau so lange, auch wenn du den Header später wieder entfernst.includeSubDomainsdehnt die Regel auf alle Subdomains aus. Das ist der Teil, der Projekte aussperrt: Ein internesintranet.deinshop.deoder ein Staging-System ohne gültiges Zertifikat ist danach für jeden Browser unerreichbar, der die Hauptdomain einmal besucht hat.preloadmeldet die Domain für die fest in Chrome, Firefox, Safari und Edge eingebaute Liste an. Dann greift HSTS schon beim allerersten Besuch. Die Betreiber der Liste schreiben selbst, dass eine Aufnahme sich nicht ohne Weiteres rückgängig machen lässt und das Entfernen Monate dauert.
Für eine Anwendung, die heute noch keinen HSTS-Header sendet, empfiehlt hstspreload.org einen Stufenplan: Erst alle Subdomains auf HTTPS bringen und die HTTP-zu-HTTPS-Umleitung prüfen. Dann max-age=300 setzen (fünf Minuten) und beobachten. Dann eine Woche (604800), dann ein Monat (2592000), dann zwei Jahre. Zwischen den Stufen jeweils die volle Laufzeit abwarten. Erst wenn der Zwei-Jahres-Wert mit includeSubDomains mehrere Wochen ohne Vorfall läuft, kommt preload dazu und die Domain wird eingereicht.
Bei einem Shopware-6-Shop ist der Stufenplan für die HTML-Seiten bereits hinfällig: Der Core sendet auf jeder erfolgreichen HTTPS-Antwort max-age=31536000; includeSubDomains, und laut RFC 6797 wertet der Browser nur den ersten HSTS-Header einer Antwort aus. Ein Jahr mit includeSubDomains ist dort also schon live, egal was der Webserver zusätzlich setzt. Die Konsequenz: Subdomains ohne HTTPS sperren bei Shopware-Shops heute schon Besucher aus, die die Hauptdomain einmal aufgerufen haben. Der Webserver-Wert sollte dann denselben oder einen längeren max-age tragen, damit statische Dateien und Fehlerseiten nicht eine kürzere Regel ausliefern.
Browser werten HSTS nur aus, wenn der Header über HTTPS kommt; über HTTP wird er ignoriert, also muss die Umleitung zuerst stehen. Und HSTS gehört bei einem Plattformwechsel auf die Checkliste neben die 301-Redirects, sonst bleiben die alten http://-Links aus Preisvergleichen und Newslettern ein offenes Fenster; der Beitrag zur Shop-Migration ohne SEO-Verlust behandelt die Redirect-Seite davon.
Snippets: Nginx, Caddy und die Shopware-.htaccess
Die folgenden Blöcke setzen die fünf gefahrlosen Header plus HSTS. Der HSTS-Wert entspricht dem, was Shopware selbst sendet; bei einer Anwendung ohne eigenen HSTS-Header startest du stattdessen mit max-age=300 aus dem Stufenplan. Die Content Security Policy fehlt bewusst; sie kommt im nächsten Abschnitt als Report-Only-Variante dazu.
Nginx
Vor dem Code die Falle, die in Nginx-Konfigurationen am häufigsten steckt: add_header-Direktiven werden laut Nginx-Dokumentation nur dann vom übergeordneten Block geerbt, wenn der aktuelle Block keine eigene add_header-Direktive enthält. Ein einzelnes add_header Cache-Control in location ~* \.(css|js)$ entfernt damit alle Security Header für CSS und JavaScript, ohne Fehlermeldung.
# Im server-Block (HTTPS). add_header in einem location-Block
# ersetzt ALLE add_header des server-Blocks: entweder nur hier
# setzen oder in jedem location mit eigenem add_header wiederholen.
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), usb=()" always;
add_header Cross-Origin-Opener-Policy "same-origin-allow-popups" always;
server_tokens off;
Der Zusatz always sorgt dafür, dass die Header auch auf Fehlerseiten gesetzt werden. Ohne ihn gelten sie nur für die Statuscodes 200, 201, 204, 206, 301, 302, 303, 304, 307 und 308; eine 404-Seite oder ein 500er wären ohne Schutz.
Caddy
deinshop.de {
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options "DENY"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
Permissions-Policy "camera=(), microphone=(), geolocation=(), usb=()"
Cross-Origin-Opener-Policy "same-origin-allow-popups"
-Server
}
reverse_proxy shopware:8000
}
Caddy besorgt Zertifikate automatisch und leitet HTTP auf HTTPS um, setzt aber keinen HSTS-Header von sich aus und schickt standardmäßig Server: Caddy mit. Das -Server entfernt die Kennung. Wer Shopware oder andere Dienste mit Coolify self-hosted betreibt, setzt die Header am dort vorgeschalteten Proxy (Traefik oder Caddy), nicht im Container dahinter.
Apache: die Shopware-.htaccess
Shopware liefert mit dem Composer-Setup eine public/.htaccess aus. Alles zwischen # BEGIN Shopware und # END Shopware wird bei Updates neu generiert; eigene Direktiven gehören deshalb außerhalb der Marker, am besten darunter.
# public/.htaccess – NACH dem Block "# END Shopware" einfügen
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" env=HTTPS
Header always set X-Frame-Options "DENY"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), usb=()"
Header always set Cross-Origin-Opener-Policy "same-origin-allow-popups"
Header unset X-Powered-By
</IfModule>Das env=HTTPS verhindert, dass der HSTS-Header auf einer HTTP-Antwort landet. Terminiert ein vorgeschalteter Proxy das TLS, ist die Variable HTTPS im Apache unter Umständen nie gesetzt; dann greift die Bedingung nicht und der Header fehlt. In dem Fall gehört HSTS an den Proxy. Header always set gilt wie always in Nginx auch für Fehlerseiten. ServerTokens Prod lässt sich nur in der Hauptkonfiguration des Apache setzen, nicht in der .htaccess.
Was Shopware 6 selbst setzt
Ein Blick in den Shopware-Core relativiert den Aufwand: Der CoreSubscriber setzt auf jede erfolgreiche Antwort vier Header, bevor sie den Webserver erreicht.
| Header | Shopware-Storefront | Webserver nötig? |
|---|---|---|
Strict-Transport-Security |
max-age=31536000; includeSubDomains, nur wenn Shopware den Request als HTTPS erkennt |
Ja, für statische Dateien und als Absicherung hinter Proxys |
X-Frame-Options |
deny |
Ja, für statische Dateien |
X-Content-Type-Options |
nosniff |
Ja, für statische Dateien |
Referrer-Policy |
strict-origin-when-cross-origin |
Ja, für statische Dateien |
Content-Security-Policy |
Storefront: leer. Administration: Nonce-basiert | Nur für den Storefront, wenn gewünscht |
Permissions-Policy, COOP |
nicht gesetzt | Ja |
Die Header gelten nur für Antworten, die durch PHP laufen. Theme-Dateien, Medien und Thumbnails liefert der Webserver direkt aus, und auf diesen Antworten fehlt alles, was der Core setzt. Das ist der Grund, warum die Snippets oben trotz Shopware-Core nötig sind.
Der zweite Grund ist die HTTPS-Erkennung. Shopware prüft mit isSecure(), ob der Request verschlüsselt ankam. Hinter einem Load Balancer oder Cloudflare stimmt das nur, wenn TRUSTED_PROXIES korrekt konfiguriert ist. Konkret: Der Proxy terminiert TLS und spricht intern HTTP mit dem Shopware-Container; TRUSTED_PROXIES ist nicht gesetzt. Shopware sieht einen HTTP-Request, lässt HSTS weg, und der Shop sendet den Header nur dort, wo der Webserver ihn setzt. Setzt auch der Webserver keinen HSTS-Header, fällt das im Browser nicht auf; der Observatory-Bericht zeigt es.
Doppelte Header mit gleichem Wert sind kein Problem; bei HSTS zählt nach RFC 6797 ohnehin nur der erste. Deshalb stehen in den Snippets dieselben Werte wie im Core: DENY statt SAMEORIGIN, ein Jahr statt fünf Minuten.
Content Security Policy: Warum eine kaputte CSP den Shop kaputt macht
Die Content Security Policy ist der wirksamste und der gefährlichste Header in der Liste. Sie sagt dem Browser, aus welchen Quellen Skripte, Styles, Bilder, Frames und Verbindungen erlaubt sind. Alles, was nicht auf der Liste steht, wird nicht geladen.
Genau das ist das Problem in einem Shop: Der Checkout-Button von PayPal, das Stripe-Eingabefeld, der Google Tag Manager, der Consent-Banner und die Bewertungs-Widgets sind aus Sicht einer CSP fremde Skripte. Ein script-src 'self' ohne diese Hosts bedeutet: Die Seite lädt, der Warenkorb funktioniert, und im Checkout erscheint kein einziger Zahlungsbutton. Im Browser-Log steht die Ursache, im Monitoring steht nur ein Umsatzeinbruch.
Warum Shopware keine Storefront-CSP mitliefert
Für Shopware kommt eine zweite Hürde dazu. Der Core liefert für den Storefront bewusst keine CSP aus; der Parameter shopware.security.csp_templates enthält für storefront einen leeren String, während die Administration eine Nonce-basierte Policy und API-Routen ein restriktives Default-Template bekommen. Gleichzeitig schreibt der Storefront in jede Seite Inline-Skripte, etwa window.activeNavigationId und die Router-Konfiguration in layout/meta.html.twig. Ein script-src ohne 'unsafe-inline' oder ohne passende Nonce bricht den Storefront damit auf jeder Seite.
Shopware erzeugt zwar pro Request eine Nonce im Request-Attribut _cspNonce, die Storefront-Templates tragen sie aber nicht in die <script>-Tags ein. Eine echte Nonce-CSP für den Storefront ist deshalb ein Template-Projekt, kein Header-Handgriff.
Die Zahlen bestätigen, wie schwer das in der Praxis fällt. Laut Web Almanac 2025 enthalten 92 Prozent der ausgelieferten CSPs mit script-src ein 'unsafe-inline', Nonces nutzen rund 20 Prozent, 'strict-dynamic' rund 10 Prozent.
Und bereits 2016 zeigte ein Google-Forschungsteam in der Studie „CSP Is Dead, Long Live CSP!" an 26.011 untersuchten Policies, dass 94,68 Prozent der Policies, die Skripte einschränken wollen, umgehbar sind. Der Grund sind Host-Allowlists, die irgendwo einen JSONP-Endpunkt oder eine Angular-Bibliothek enthalten. Die daraus abgeleitete Empfehlung ist die Strict CSP mit Nonce und 'strict-dynamic'.
Der Weg zur ersten CSP: Report-Only, sichere Direktiven, Hostliste
Für einen Shop ohne CSP heißt das: zuerst Report-Only. Der Header Content-Security-Policy-Report-Only blockiert nichts, meldet aber jede Verletzung an die Browser-Konsole und an einen Report-Endpunkt. Zwei Wochen Report-Only über alle Seitentypen (Startseite, Kategorie, Produkt, Warenkorb, Checkout, Kontoseiten, Bestellbestätigung) zeigen, welche Hosts wirklich gebraucht werden. So lange dauert es, bis auch seltene Zahlungswege, Newsletter-Links und Kontoseiten, die nur alle paar Tage jemand öffnet, einmal durchgelaufen sind. Erst danach wird aus -Report-Only die scharfe Policy.
Die sicheren Direktiven sofort, script-src als Projekt. object-src 'none', base-uri 'self', frame-ancestors 'self' und form-action 'self' brechen in einem normalen Shop nichts und schließen jeweils eine konkrete Angriffsklasse. frame-ancestors ersetzt dabei X-Frame-Options in allen aktuellen Browsern; beide zusammen zu setzen ist die saubere Übergangslösung. Das script-src kommt mit den Hosts der eingesetzten Dienste dazu, vorerst mit 'unsafe-inline', weil der Storefront es braucht.
Die Hosts stammen aus der Dokumentation der Anbieter. PayPal nennt für sein JS SDK *.paypal.com, *.paypalobjects.com und *.venmo.com für script-src, frame-src, connect-src, img-src und style-src. Stripe listet js.stripe.com, *.js.stripe.com, hooks.stripe.com und api.stripe.com. Google Tag Manager braucht www.googletagmanager.com und je nach Tags weitere Hosts; welche Tracking-Skripte überhaupt laufen dürfen, regelt ohnehin der Consent, dazu mehr im Beitrag zu Cookies, Consent und TDDDG.
Eine Startpolicy für einen Shopware-Storefront mit PayPal und Google Tag Manager sieht dann so aus, zunächst als Report-Only:
# Eine Direktive pro Zeile, über eine Variable zusammengesetzt
set $csp "default-src 'self'; ";
set $csp "${csp}script-src 'self' 'unsafe-inline' *.paypal.com *.paypalobjects.com *.venmo.com www.googletagmanager.com; ";
set $csp "${csp}style-src 'self' 'unsafe-inline' *.paypal.com *.paypalobjects.com *.venmo.com; ";
set $csp "${csp}img-src 'self' data: *.paypal.com *.paypalobjects.com *.venmo.com www.googletagmanager.com *.google-analytics.com; ";
set $csp "${csp}frame-src *.paypal.com *.paypalobjects.com *.venmo.com; ";
set $csp "${csp}connect-src 'self' *.paypal.com *.paypalobjects.com *.venmo.com *.google-analytics.com analytics.google.com; ";
set $csp "${csp}font-src 'self' data:; ";
set $csp "${csp}object-src 'none'; base-uri 'self'; frame-ancestors 'self'; ";
set $csp "${csp}form-action 'self' *.paypal.com; ";
set $csp "${csp}report-uri https://deinshop.report-uri.com/r/d/csp/reportOnly";
add_header Content-Security-Policy-Report-Only $csp always;
form-action braucht die Payment-Hosts, wenn ein Zahlungsweg per Formular-Redirect arbeitet (3-D Secure, Rechnungskauf-Anbieter, Sofortüberweisung). Das ist ein typischer Report-Only-Fund und in einer scharfen Policy besonders teuer: Der Kunde klickt „Jetzt kaufen", der Browser verweigert den Redirect, und die Zahlung kommt nicht zustande. In der Konsole steht dann eine Zeile wie Refused to send form data to 'https://www.paypal.com/…' because it violates the following Content Security Policy directive: "form-action 'self'".
In 30 Minuten: das Vorgehen
- Ist-Zustand messen (5 Minuten): Die Domain beim HTTP Observatory von MDN oder bei securityheaders.com eintragen und das Ergebnis sichern.
- Die fünf gefahrlosen Header setzen (10 Minuten): Snippet für den eigenen Webserver übernehmen, Server-Kennung entfernen, Konfiguration neu laden.
- HSTS (5 Minuten): Bei Shopware den Core-Wert am Webserver spiegeln; bei anderen Anwendungen
max-age=300; includeSubDomains, nur über HTTPS. Vorher die Subdomains durchgehen, die noch HTTP sprechen. - CSP als Report-Only (10 Minuten): Startpolicy mit den Hosts der eigenen Payment- und Tracking-Anbieter anlegen, Report-Endpunkt eintragen, Checkout einmal durchklicken, Konsole lesen.
- Nachmessen: Observatory erneut laufen lassen und mit dem gesicherten Ergebnis vergleichen. HSTS auf zwei Jahre und das Scharfschalten der CSP folgen nach Wochen, nicht nach Minuten.
Damit ist die Client-Seite erledigt: Jeder Browser, der deinen Shop aufruft, bekommt die Regeln, die ihn vor Downgrade, Clickjacking und eingeschleusten Skripten schützen. Was die Header nicht abdecken, also Plugin-Stand, Admin-Zugänge, Cache und Datenbank, prüft ein Shopware-Audit.
Häufige Fragen zu Security Headern
Reicht die Umleitung von HTTP auf HTTPS nicht aus?
Nein. Die Umleitung passiert erst, nachdem der Browser den unverschlüsselten Request abgeschickt hat. HSTS verhindert genau diesen ersten Request, bei jedem Besuch nach dem ersten.
Wie prüfe ich, ob die Header wirklich ankommen?
Mit curl -I https://deinshop.de oder in den DevTools des Browsers unter Network → Response Headers. Wichtig: nicht nur die Startseite prüfen, sondern auch eine CSS-Datei, ein Produktbild und eine 404-Seite, weil statische Dateien und Fehlerseiten am häufigsten durchrutschen.
Ich habe includeSubDomains gesetzt und ein internes System ohne HTTPS ist nicht mehr erreichbar. Was jetzt?
Den Header auf max-age=0 setzen und die Hauptdomain einmal im betroffenen Browser aufrufen; damit löscht der Browser den Eintrag. Das wirkt nur pro Browser und nur, wenn die Domain nicht in der Preload-Liste steht. Bei Shopware-Shops greift dieser Rückbau am Webserver nicht, weil der Core-Header zuerst steht und ein Jahr mit includeSubDomains vorgibt. Dauerhaft hilft nur, das interne System auf HTTPS zu bringen.
Mein Hoster oder Cloudflare setzt schon Header. Muss ich trotzdem?
Prüfen statt annehmen. Cloudflare setzt HSTS nur, wenn es im Dashboard aktiviert wurde, und keine CSP. Wenn der Proxy Header setzt, gilt derselbe Grundsatz wie zwischen Shopware und Webserver: ein Wert pro Header, an einer Stelle gepflegt.
Warum nicht gleich die strenge OWASP-CSP übernehmen?
Weil default-src 'self' ohne Payment-, Tracking- und Font-Hosts in einem Shop den Checkout stoppt. Die OWASP-Werte sind das Ziel für eine Anwendung ohne Drittanbieter-Skripte. Ein Shop erreicht sie über Report-Only, Hostliste und, wenn es sauber werden soll, über Nonces in den Templates.