Security-Header erklärt: CSP, HSTS und der Rest
Ein Praxisleitfaden zu CSP mit Nonces und strict-dynamic, HSTS und Preload, frame-ancestors, Referrer-Policy, Permissions-Policy und COOP, mit Next.js-Beispiel.
· 7 Min. Lesezeit · Lina Source LLC
Security-Header sind Anweisungen, die Ihr Server dem Browser gibt: Führe nur diese Skripte aus, sprich mit mir nur über HTTPS, lass andere Websites diese Seite nicht in einem Frame einbetten. Sie beheben keine Fehler in Ihrem Code, aber sie begrenzen, was ein Angreifer mit einem solchen Fehler anfangen kann. Eine Cross-Site-Scripting-Lücke hinter einer strikten Content Security Policy ist ein deutlich kleineres Problem als dieselbe Lücke ohne sie.
Die meisten dieser Header sind eine Zeile Konfiguration. Die Ausnahme ist CSP, die Planung erfordert. Dieser Leitfaden zeigt, was jeder Header bewirkt, welche Werte für eine typische Web-App sinnvoll sind und wie Sie sie ausrollen, ohne die Produktion zu beschädigen.
Content-Security-Policy
CSP teilt dem Browser mit, welche Quellen für Skripte, Styles, Bilder, Frames und Verbindungen erlaubt sind. Ihre Hauptaufgabe ist es, eingeschleuste Skripte am Ausführen zu hindern. Eine Allowlist von Domains wirkt wie der naheliegende Ansatz, hat aber eine lange Geschichte von Umgehungen: Jedes erlaubte CDN, das Nutzerinhalte oder alte Bibliotheksversionen hostet, lässt sich nutzen, um vom Angreifer kontrolliertes Skript zu laden.
Nonces und strict-dynamic
Der Ansatz, der hält, ist eine Nonce-basierte Policy. Der Server erzeugt pro Antwort einen Zufallswert, setzt ihn in den CSP-Header und fügt ihn jedem Script-Tag, das er rendert, als nonce-Attribut hinzu. Eingeschleuste Script-Tags kennen die Nonce nicht und werden blockiert. Mit 'strict-dynamic' dürfen Skripte, die von einem vertrauenswürdigen Skript geladen wurden, weitere Skripte nachladen, sodass Bundler und Tag-Manager funktionieren, ohne dass Sie jede Domain auflisten müssen.
Content-Security-Policy:
default-src 'self';
script-src 'nonce-4AEemGb0xJptoIGFP3Nd' 'strict-dynamic';
style-src 'self' 'nonce-4AEemGb0xJptoIGFP3Nd';
img-src 'self' data: https:;
connect-src 'self';
object-src 'none';
base-uri 'none';
frame-ancestors 'none';
form-action 'self';
upgrade-insecure-requestsDer Header wird in einer einzigen Zeile gesendet; hier ist er zur besseren Lesbarkeit umbrochen. Einige Direktiven in dieser Policy leisten mehr, als es auf den ersten Blick scheint. object-src 'none' blockiert alte Plugin-Inhalte. base-uri 'none' verhindert, dass ein eingeschleustes base-Tag relative Skript-URLs umlenkt. form-action 'self' verhindert, dass eingeschleuste Formulare Zugangsdaten anderswohin senden. Die Nonce muss unvorhersehbar und bei jeder Antwort neu sein, weshalb Seiten, die sie verwenden, nicht aus einem statischen Cache ausgeliefert werden können.
Mit Report-Only ausrollen
Eine strikte CSP, die blind ausgerollt wird, beschädigt garantiert etwas: ein Analytics-Snippet, einen Inline-Event-Handler, ein Widget von Dritten. Liefern Sie die Policy zunächst als Content-Security-Policy-Report-Only aus. Der Browser erzwingt dann nichts, meldet aber jeden Verstoß an den Endpunkt, der in der report-to- oder report-uri-Direktive genannt ist. Sammeln Sie eine Weile Meldungen, beheben oder erlauben Sie, was legitim ist, und stellen Sie dann den Header-Namen auf Durchsetzung um. Lassen Sie das Reporting auch nach der Durchsetzung aktiv, denn neue Verstöße sind entweder Regressionen oder Angriffe. Rechnen Sie mit Rauschen durch Browser-Erweiterungen, die eigene Skripte in Seiten einfügen; filtern Sie diese nach Quelle heraus, bevor Sie entscheiden, was Sie erlauben.
Strict-Transport-Security
HSTS weist den Browser an, Ihre Domain für einen festgelegten Zeitraum ausschließlich über HTTPS aufzurufen, selbst wenn ein Benutzer http:// eintippt oder einen alten Link anklickt. Damit schließt sich das Zeitfenster, in dem ein Angreifer im Netzwerk die erste unverschlüsselte HTTP-Anfrage abfangen und die Weiterleitung auf HTTPS entfernen könnte. Browser beachten den Header nur, wenn er über HTTPS eintrifft, und sie lassen Benutzer auf einer HSTS-Domain auch nicht durch Zertifikatsfehler hindurchklicken. Genau das ist der Sinn, aber auch das Risiko, wenn ein Zertifikat ausläuft.
Beginnen Sie mit einem kurzen max-age, etwa einem Tag, prüfen Sie, dass nichts kaputtgeht, und erhöhen Sie es dann auf ein oder zwei Jahre. includeSubDomains erweitert die Regel auf jede Subdomain; stellen Sie daher zuerst sicher, dass alle gültiges HTTPS ausliefern, einschließlich alter Marketing-Seiten und interner Tools auf derselben Domain.
Die Direktive preload verankert Ihre Domain zusammen mit einer Einreichung in die Preload-Liste der Browser fest in diesen, sodass schon der allererste Besuch über HTTPS läuft. Sie erfordert ein max-age von mindestens einem Jahr und includeSubDomains. Betrachten Sie das als Einbahnstraße: Eine Entfernung aus der Liste ist möglich, erreicht die Benutzer aber erst nach langer Zeit, und jede Subdomain, die kein HTTPS kann, ist in der Zwischenzeit nicht erreichbar.
Die Einzeiler-Header
- X-Content-Type-Options: nosniff. Hindert Browser daran, einen anderen Content-Type zu erraten als den, den Sie deklariert haben. Das verhindert, dass eine hochgeladene Datei, die als Text ausgeliefert wird, als Skript ausgeführt wird.
- frame-ancestors (in der CSP) und X-Frame-Options: DENY. Steuern, wer Ihre Seiten in einem Frame einbetten darf; das ist die Abwehr gegen Clickjacking. Moderne Browser verwenden frame-ancestors und ignorieren X-Frame-Options, wenn beide gesetzt sind; behalten Sie X-Frame-Options für ältere Clients. Verwenden Sie 'self' oder SAMEORIGIN, wenn Sie eigene Seiten framen.
- Referrer-Policy: strict-origin-when-cross-origin. Sendet an andere Websites nur Ihre Origin, nicht den vollständigen Pfad und Query-String. So gelangen Tokens und IDs aus URLs nicht zu Dritten. Verwenden Sie no-referrer für besonders sensible Seiten.
- Permissions-Policy. Schaltet Browser-Funktionen ab, die Sie nicht nutzen, etwa camera=(), microphone=(), geolocation=() und payment=(), sodass eingeschleuster oder eingebetteter Code sie nicht anfordern kann.
- Cross-Origin-Opener-Policy: same-origin. Stellt Ihre Seite in eine eigene Browsing-Context-Gruppe, sodass ein von einer anderen Website geöffnetes Fenster keine Referenz darauf behalten kann. Verwenden Sie same-origin-allow-popups, wenn Sie auf OAuth- oder Zahlungs-Popups angewiesen sind.
- Cross-Origin-Resource-Policy: same-origin oder same-site. Weist Browser an, andere Origins Ihre Antworten nicht als Bilder, Skripte oder sonstige Subressourcen laden zu lassen. Verwenden Sie same-site, wenn Sie Assets von einer Schwester-Subdomain ausliefern, und cross-origin für Assets, die anderswo eingebettet werden sollen.
X-XSS-Protection können Sie weglassen. Der Filter, den dieser Header gesteuert hat, wurde aus modernen Browsern entfernt, und CSP ist der Ersatz. Besteht ein Scanner darauf, setzen Sie ihn auf 0. Vermeiden Sie ebenso Header, die einem Angreifer nur Informationen liefern: X-Powered-By und ausführliche Server-Banner verraten Ihr Framework und dessen Version gratis. In Next.js entfernen Sie den ersten, indem Sie poweredByHeader: false in der Konfiguration setzen.
Eine Next.js-Konfiguration
Statische Header gehören in next.config.ts, wo die Funktion headers() sie auf jede Route anwendet. Die folgenden Werte sind ein sinnvoller Standard für eine App, die sich nicht selbst in Frames einbettet, weder Kamera noch Standort nutzt und ihre eigenen Assets ausliefert. Passen Sie jeden Wert daran an, was Ihre App tatsächlich tut, statt sie blind zu übernehmen.
// next.config.ts
import type { NextConfig } from "next";
const securityHeaders = [
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains",
},
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=(), payment=()",
},
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
{ key: "Cross-Origin-Resource-Policy", value: "same-origin" },
];
const nextConfig: NextConfig = {
async headers() {
return [{ source: "/(.*)", headers: securityHeaders }];
},
};
export default nextConfig;Die CSP braucht pro Request eine frische Nonce und wird deshalb stattdessen in der Middleware gesetzt. Next.js liest die Nonce aus dem Content-Security-Policy-Header des Requests und wendet sie beim Rendern auf die frameworkeigenen Skripte an. In einer Server-Komponente können Sie sie mit headers().get("x-nonce") für Ihre eigenen Script-Tags auslesen.
// middleware.ts (in Next.js 16 heißt die Datei proxy.ts und die Funktion proxy)
import { NextResponse, type NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
const csp = [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'nonce-${nonce}'`,
"img-src 'self' blob: data:",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
"upgrade-insecure-requests",
].join("; ");
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-nonce", nonce);
requestHeaders.set("Content-Security-Policy", csp);
const response = NextResponse.next({ request: { headers: requestHeaders } });
response.headers.set("Content-Security-Policy", csp);
return response;
}
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};Seiten, die mit einer Nonce gerendert werden, müssen dynamisch gerendert werden, da eine statische Seite dieselbe Nonce für jeden Besucher wiederverwenden würde. Ändern Sie während des Rollouts an beiden Stellen den Header-Namen in Content-Security-Policy-Report-Only und ergänzen Sie einen Reporting-Endpunkt. In der Entwicklung benötigt React für seine Debugging-Funktionen möglicherweise 'unsafe-eval' in script-src; fügen Sie es nur hinzu, wenn NODE_ENV auf development steht.
So verifizieren Sie
- Rufen Sie curl -sI https://your-domain.example/ für die Startseite, eine dynamische Seite, eine API-Route und ein statisches Asset auf und lesen Sie jeweils die Header. Header, die nur auf HTML-Seiten gesetzt werden, sind eine häufige Lücke.
- Öffnen Sie die Entwicklerwerkzeuge des Browsers. CSP-Verstöße erscheinen in der Konsole mit der Direktive und der blockierten Ressource, und der Netzwerk-Tab zeigt die tatsächlich ausgelieferten Header.
- Prüfen Sie hinter Ihrem CDN oder Reverse Proxy ebenso wie am Origin. Proxys entfernen, duplizieren oder überschreiben Header mitunter, und zwei widersprüchliche CSP-Header werden beide durchgesetzt, sodass die effektive Policy strenger ist als jede einzelne für sich.
- Nutzen Sie einen öffentlichen Checker wie den Mozilla HTTP Observatory für eine schnelle zweite Meinung und Googles CSP Evaluator, um schwache CSP-Direktiven zu erkennen.
- Ergänzen Sie einen Test in der CI, der zentrale Routen abruft und prüft, dass die Header vorhanden sind, damit ein Refactoring der Konfiguration sie nicht unbemerkt entfernen kann.
Header sind Konfiguration, und genau deshalb driften sie: ein neuer Route Handler, der seine eigene Antwort baut, eine Proxy-Regel, die die Standardwerte überschreibt, eine CSP, die mit 'unsafe-inline' gelockert wurde, um ein Release freizubekommen. Prüfen Sie sie wie Code. CodeAuditAgent liest die Konfiguration in einem öffentlichen Repository oder einem eingefügten Snippet zusammen mit dem übrigen Code und meldet fehlende oder abgeschwächte Header mit Schweregrad, der zitierten Konfiguration und einem Lösungsvorschlag.
Eine sinnvolle Reihenfolge der Arbeit: die Einzeiler-Header heute, HSTS mit kurzem max-age in dieser Woche und eine Nonce-basierte CSP im Report-Only-Modus, sobald Sie die Meldungen einsammeln können.