CSP-Validator

Die meisten Policies erlauben mehr, als ihre Autoren denken.

Füge eine Content-Security-Policy ein und sieh, was ein Browser tatsächlich durchlässt. Uns wird nichts geschickt: die Policy wird in deinem Browser ausgewertet, du kannst also einen Header prüfen, den du noch gar nicht deployed hast.

Läuft in deinem Browser. Uns wird nichts geschickt.

Was hier geprüft wird, und wie eine gute Antwort aussieht

Welche Direktive deine Skripte wirklich regelt

Browser nehmen script-src, wenn es da ist, und fallen sonst auf default-src zurück. Dieser Rückfall überrascht häufig: eine Policy mit strengem default-src und großzügigem script-src ist genauso großzügig wie das zweite, und die strenge Zeile darüber tut für Skripte gar nichts. Wir zeigen dir, welche Direktive wir gelesen haben, damit das Urteil überprüfbar ist statt etwas, das du glauben musst.

unsafe-eval

Erlaubt, Strings in ausführbaren Code zu verwandeln, und darauf setzen die meisten Injection-Payloads auf. Meist ist es da, weil eine Bundler-Einstellung oder eine Template-Bibliothek danach verlangt hat, und meist lässt es sich durch Ändern dieser Einstellung entfernen. Solange es da ist, ist ein großer Teil des Rests der Policy Dekoration.

Wildcards, und wann sie nicht zählen

Ein einzelnes * oder eine Schema-Quelle wie https: erlaubt Skripte von jedem Host im Internet. Aber eine Host-Wildcard neben 'strict-dynamic' ist kein Loch, denn dieses Schlüsselwort sagt dem Browser, Host-Quellen komplett zu ignorieren und nur dem zu vertrauen, was ein per Nonce freigegebenes Skript nachlädt. Wir prüfen ganze Tokens, statt im Text zu suchen, sodass https://cdn.beispiel.de/* korrekt als konkreter Host gelesen wird und nicht als Wildcard, nur weil ein Stern darin vorkommt.

unsafe-inline, und wann es ohnehin ignoriert wird

Der häufigste Grund, warum eine streng wirkende Policy nicht hält. Sie erlaubt Inline-Skripte, also wird injiziertes Markup ausgeführt. Die Feinheit, über die die meisten Prüfer stolpern: trägt dieselbe Direktive eine Nonce oder einen Hash, ignorieren Browser 'unsafe-inline' vollständig. Policies werden bewusst so geschrieben, damit ältere Browser, die Nonces nicht verstehen, die Seite trotzdem laden. Diese Form zu markieren wäre ein Fehlalarm auf der häufigsten korrekten strengen Policy überhaupt, also melden wir sie als in Ordnung und sagen, warum.

base-uri, die fast immer fehlt

base-uri wird von default-src nicht abgedeckt, eine Policy kann also vollständig wirken und sie trotzdem auslassen. Ohne sie biegt ein injiziertes <base>-Tag jedes relative Skript und jeden relativen Link der Seite auf einen anderen Host um, was aus einer kleinen HTML-Injection eine vollständige Skriptausführung macht und sauber an einem script-src vorbeigeht, an dem du lange gearbeitet hast. Es kostet eine Direktive.

object-src und frame-ancestors

object-src 'none' schließt Plugin-Inhalte, die kaum eine Anwendung braucht und die sonst ein Dokument in deiner Origin ausführen können. frame-ancestors entscheidet, wer deine Seite einbetten darf; es ist der moderne Ersatz für X-Frame-Options und das einzige der beiden, das eine Liste erlaubter Origins ausdrücken kann.

So behebst du es

Eine strenge Policy ist leichter zu erreichen, als ihr Ruf vermuten lässt, solange du sie zuerst im Report-only-Modus ausrollst und liest, was bricht. Der Nonce-Ansatz unten ist der, den diese Seite selbst verwendet, es ist also ein Muster, das wir in Produktion betreiben, und nicht bloß eines, das wir empfehlen.

Eine strenge Policy zum Anfangen
Content-Security-Policy:
  default-src 'self';
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data:;
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none'

# 'strict-dynamic' lets a nonce-approved script load what it needs
# without you listing every CDN, which is what makes a strict policy
# survivable in a real app with a bundler.
Zuerst gefahrlos ausrollen
# Same policy, different header name. Browsers report violations
# to the console and to report-to, and block NOTHING. Run it for a
# week, read what shows up, then rename the header.
Content-Security-Policy-Report-Only:
  default-src 'self'; script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'
Die Nonce erzeugen (Cloudflare Worker)
// The nonce must be new on EVERY request. A nonce baked into a
// static file is not a nonce, it is a password the attacker also has.
const nonce = crypto.randomUUID().replaceAll("-", "");

const policy = [
  "default-src 'self'",
  `script-src 'nonce-${nonce}' 'strict-dynamic'`,
  "object-src 'none'",
  "base-uri 'self'",
  "frame-ancestors 'none'",
].join("; ");

// Then stamp the same nonce onto every <script> as you stream the HTML.
return new HTMLRewriter()
  .on("script", { element: (el) => el.setAttribute("nonce", nonce) })
  .transform(new Response(html, { headers: { "content-security-policy": policy } }));
Next.js-Middleware
// middleware.ts
import { NextResponse } from "next/server";

export function middleware(request: Request) {
  const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
  const policy = `default-src 'self'; script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'self'`;

  const headers = new Headers(request.headers);
  headers.set("x-nonce", nonce);

  const response = NextResponse.next({ request: { headers } });
  response.headers.set("content-security-policy", policy);
  return response;
}
Was dieses Tool nicht sehen kann

Das hier liest die Policy, die du eingefügt hast. Es kann dir nicht sagen, ob das die Policy ist, die dein Server wirklich schickt, und die beiden weichen öfter voneinander ab, als man erwartet: ein Proxy, eine CDN-Regel oder ein zweiter Header auf Framework-Ebene können sie überschreiben oder verdoppeln, und wenn zwei Policies ankommen, setzt der Browser die Schnittmenge beider durch. Es kann dir auch nicht sagen, ob die Policy deine App bricht, das zeigt nur ein Report-only-Rollout. Um zu sehen, was deine Live-Seite schickt, lass den Security-Header-Checker auf die URL los.

Auch den Code scannenZum Einstieg kostenlos. Repository verbinden, der erste Scan läuft in rund zwei Minuten.