
Content-Security-Policy-Report-Only: erst beobachten, dann scharf schalten
Content-Security-Policy-Report-Only ist dieselbe Richtlinie wie Content-Security-Policy, nur ohne Folgen: Der Browser prüft jede Ressource gegen die Regeln, blockiert aber nichts und schickt stattdessen einen Bericht. Die Spezifikation nennt den Zweck ausdrücklich — die Kopfzeile erlaubt, „Richtlinien durch Beobachten (nicht Erzwingen) ihrer Wirkung auszuprobieren", und beschreibt den Weg gleich mit: eine Report-Only-Richtlinie nach bester Schätzung ausrollen, Verstoßberichte lesen, dann zur erzwungenen Richtlinie wechseln (W3C, CSP Level 3, § 3.2).
Ohne Berichtsziel ist die Kopfzeile wirkungslos. Sie blockiert nichts, und sie meldet auch nichts, wenn in der Richtlinie kein report-to oder report-uri steht. MDN formuliert es knapp: Ohne report-to „hat der Vorgang keine Wirkung" (MDN, Content-Security-Policy-Report-Only). Wer die Kopfzeile setzt und wartet, wartet auf nichts.
Beide Kopfzeilen gleichzeitig#
Der Normalfall auf einer laufenden Seite: Es gibt schon eine scharfe Richtlinie, etwa eine mit 'unsafe-inline', und die soll strenger werden. Dann bleibt die alte in Content-Security-Policy stehen und die neue kommt in Content-Security-Policy-Report-Only daneben. Beide werden beachtet — die erste erzwingt, die zweite erzeugt Berichte (MDN, CSP-Leitfaden). Die Spezifikation legt das allgemein fest: Auf eine Ressource können mehrere Richtlinien angewendet werden, und jede hat eine Disposition, entweder enforce oder report (CSP3, § 2.2).
Content-Security-Policy: script-src 'self' 'unsafe-inline'
Content-Security-Policy-Report-Only: script-src 'self' 'nonce-…'; report-to csp
Reporting-Endpoints: csp="https://www.example.at/csp-berichte"
Genau dieses disposition-Feld taucht später in jedem Bericht auf. Es sagt, ob die Meldung aus der scharfen oder der beobachtenden Richtlinie stammt — wichtig, sobald beide laufen, weil sonst nicht klar ist, ob etwas tatsächlich blockiert wurde.

Wohin die Berichte gehen#
Es gibt zwei Wege, und sie liefern unterschiedliche Formate.
report-uri ist der alte. Die Direktive nennt eine Adresse, der Browser schickt je Verstoß einen POST mit Content-Type: application/csp-report. Die Spezifikation markiert sie als veraltet und schreibt dazu, dass „ein einzelner Request pro Verstoß schlicht nicht skaliert" (CSP3, § 6.5.1, § 5.5).
report-to ist der neue. Die Direktive nennt keinen Pfad, sondern einen Namen, und der Name muss in einer eigenen Kopfzeile Reporting-Endpoints auf eine Adresse zeigen (MDN, report-to). Der Browser sammelt die Berichte und liefert sie gebündelt als JSON-Liste mit Content-Type: application/reports+json; die Reporting-API sagt, der Browser holt sich „periodisch" die Warteschlange und stellt zu (W3C Reporting API, § 3.5). Berichte kommen also verzögert und in Paketen. Das Feld age im Bericht gibt an, wie viele Millisekunden seit dem Verstoß vergangen sind.
Stehen beide Direktiven in einer Richtlinie, gewinnt report-to; report-uri wird dann ignoriert. Die Spezifikation empfiehlt trotzdem beide, damit ältere Browser noch melden können (CSP3, § 6.5.1). MDN begründet das mit dem Stand der Unterstützung (MDN, report-uri):
| Chrome | Firefox | Safari | |
|---|---|---|---|
report-uri | 25 | 23 | 7 |
report-to | 70 | 149 | 16.4 |
Reporting-Endpoints | 96 | 130 | 16.4 |
Quelle: MDN browser-compat-data, Stand 14.09.2026. Firefox hat report-to erst mit Version 149 bekommen; MDN führt das Feature deshalb als „Baseline 2026, neu verfügbar seit März 2026". Wer heute nur report-to setzt, bekommt von jedem älteren Firefox keinen einzigen Bericht.
Ein Stolperstein in Reporting-Endpoints: Die Adresse muss https sein. Unsichere Endpunkte werden ignoriert, ohne Fehlermeldung (MDN, Reporting-Endpoints).
Warum das nicht per Meta-Tag geht#
Eine normale CSP kann als <meta http-equiv="Content-Security-Policy"> im Dokument stehen. Die Report-Only-Fassung nicht. Die Spezifikation ist eindeutig: „The Content-Security-Policy-Report-Only header is not supported inside a meta element" (CSP3, § 3.2). Wer keinen Zugriff auf die Serverkonfiguration hat, kann den Berichtsmodus also nicht aus dem Theme heraus nachrüsten — er braucht die Kopfzeile in der Auslieferung. Wo die hingehört, steht in Security-Header und SEO.
Ein zweiter Sonderfall: Die Direktive sandbox wird in einer Report-Only-Kopfzeile „vollständig ignoriert" (CSP3, § 6.3.2). Sie lässt sich nicht beobachten, nur erzwingen.
Wie ein Verstoßbericht aussieht#
Über report-to kommt ein Objekt vom Typ csp-violation. So sieht das Beispiel bei MDN aus (MDN, report-to):
{
"age": 53531,
"type": "csp-violation",
"url": "https://example.com/csp-report",
"user_agent": "Mozilla/5.0 …",
"body": {
"blockedURL": "inline",
"disposition": "enforce",
"documentURL": "https://example.com/csp-report",
"effectiveDirective": "script-src-elem",
"originalPolicy": "default-src 'self'; report-to csp-endpoint-name",
"referrer": "https://www.google.com/",
"sourceFile": "https://example.com/csp-report",
"lineNumber": 121,
"columnNumber": 39,
"sample": "console.log(\"lo\")",
"statusCode": 200
}
}
Die Felder sind in der Spezifikation als CSPViolationReportBody definiert: documentURL, referrer, blockedURL, effectiveDirective, originalPolicy, sourceFile, sample, disposition, statusCode, lineNumber, columnNumber (CSP3, § 5). Das alte report-uri-Format verpackt dasselbe in ein Objekt csp-report mit Bindestrich-Namen: blocked-uri, document-uri, effective-directive, violated-directive, original-policy, script-sample (MDN, report-uri). Ein Endpunkt, der beide Wege bedient, muss beide Schreibweisen kennen.
Zum Lesen reichen drei Felder: effectiveDirective sagt, welche Regel verletzt wurde, blockedURL sagt, was, sourceFile mit Zeile und Spalte sagt, wo es im eigenen Code steht.
sample ist leer, solange 'report-sample' fehlt. Erst mit diesem Schlüsselwort in der Direktive schreibt der Browser die ersten 40 Zeichen des Inline-Codes in den Bericht (CSP3, § 4.2.3). Ohne die Probe steht bei jedem Inline-Skript nur inline, und man weiß nicht, welches gemeint ist. Für die Aufräumarbeit ist das Schlüsselwort deshalb Pflicht.
Das Rauschen durch Erweiterungen#
Wer den ersten Tag Berichte liest, findet Verstöße, die es auf der eigenen Seite gar nicht gibt: blockierte Skripte, die niemand eingebaut hat, mit blockedURL-Werten wie chrome-extension oder moz-extension.
Das sind Browser-Erweiterungen, die Code in die Seite einschleusen. Die Spezifikation sagt, eine Richtlinie „sollte nicht" mit Add-ons, Erweiterungen oder Bookmarklets in Konflikt geraten, und nennt den Grund gleich mit: Auf solche Features angewendet, erzeugt CSP „eine erhebliche Menge Rauschen in Verstoßberichten". Chrome nimmt das Schema chrome-extension: deshalb ganz von der Prüfung aus (CSP3, § 9.1). Was bei anderen Browsern und anderen Injektionswegen durchkommt, zeigt sich im Bericht nur verkürzt: Für alles, was nicht http oder https ist, enthält der Bericht nur das Schema, nicht die Adresse (CSP3, § 5.4).
Daraus folgt eine Filterregel, die man beim Auswerten braucht: Ein Bericht, dessen blockedURL kein http- oder https-Wert und keines von inline, eval ist, stammt nicht von deiner Seite. Er sagt etwas über den Browser des Besuchers, nicht über deinen Code, und gehört aussortiert, bevor man zählt.

Ein Endpunkt, der Berichte annimmt#
Der Endpunkt muss wenig können: POST annehmen, den Körper speichern, mit einem Status zwischen 200 und 299 antworten. Die Reporting-API wertet genau diesen Bereich als Erfolg; eine 410 streicht den Endpunkt aus der Liste, alles andere gilt als Fehlschlag (Reporting API, § 3.5.2). Beim alten report-uri-Weg wird die Antwort ohnehin ignoriert (CSP3, § 5.5).
Ein minimales Beispiel für Node, ohne Abhängigkeiten:
import { createServer } from "node:http";
import { appendFile } from "node:fs/promises";
createServer((req, res) => {
if (req.method !== "POST" || req.url !== "/csp-berichte") {
res.writeHead(404).end(); return;
}
let body = "";
req.on("data", (chunk) => { if ((body += chunk).length > 65536) req.destroy(); });
req.on("end", async () => {
let berichte = [];
try {
const daten = JSON.parse(body);
// report-to: Liste von Berichten; report-uri: ein Objekt mit "csp-report"
berichte = Array.isArray(daten) ? daten : [daten];
} catch { res.writeHead(400).end(); return; }
const zeile = berichte.map((b) => JSON.stringify(b)).join("\n") + "\n";
await appendFile("csp-berichte.jsonl", zeile);
res.writeHead(204).end();
});
}).listen(3000);
Zwei Dinge sind darin absichtlich drin. Die Größenbegrenzung, weil jeder Browser der Welt an diese Adresse schicken darf. Und die Unterscheidung Liste oder Objekt, weil report-to eine Liste liefert und report-uri ein einzelnes Objekt. MDN weist zudem darauf hin, dass Berichte als angreifergesteuerte Daten zu behandeln und vor dem Speichern oder Anzeigen zu bereinigen sind, besonders das Feld script-sample (MDN, report-uri). Wer die Datei später in einem Browser ansieht, sollte das ernst nehmen.
Am wenigsten Ärger macht ein Endpunkt auf derselben Domain wie die Seite. Die Reporting-API schickt cross-origin ohne Anmeldedaten und im CORS-Modus (Reporting API, § 3.5.2); wer das Ziel woanders betreibt, muss sich um diese Antwortkopfzeilen kümmern.
Der Weg zum Scharfschalten#
Der Weg dauert Tage, nicht Minuten — nicht wegen der Arbeit, sondern weil die Berichte Zeit brauchen, um alle Seiten und alle Browser abzudecken.
- Endpunkt aufsetzen und mit
curl -X POSTprüfen, dass er204antwortet. Reporting-Endpointssetzen und die neue Richtlinie inContent-Security-Policy-Report-Only, mitreport-to,report-uriund'report-sample'. Die alte scharfe Richtlinie bleibt unverändert.- Prüfen, dass die Kopfzeilen ankommen. Nicht im Browser, der zwischenspeichert, sondern mit einem Abruf direkt beim Server. Der Security-Scan zeigt, welche Schutzkopfzeilen der Server tatsächlich mitschickt.
- Einige Tage sammeln. Nicht eine Stunde. Unterseiten, die selten aufgerufen werden, melden sich erst spät.
- Rauschen aussortieren: alle Berichte mit Schema statt Adresse in
blockedURLweg. - Den Rest gruppieren nach
effectiveDirectiveundblockedURL. Was übrig bleibt, ist die Liste der Dinge, die die neue Richtlinie brechen würde — eigene Inline-Skripte, Plugin-Skripte, fremde Dienste. - Aufräumen, nicht erlauben. Inline-Skripte in Dateien auslagern, Nonce oder Hash setzen, fremde Dienste einzeln freigeben. Der Weg dorthin steht in Content-Security-Policy: unsafe-inline loswerden.
- Neue Fassung wieder in Report-Only, bis die Berichte für die eigene Seite auf null sind.
- Tauschen: Die Richtlinie aus
Content-Security-Policy-Report-Onlywandert nachContent-Security-Policy, das Berichtsziel bleibt drin. Auch eine scharfe Richtlinie soll melden, denn erst dann sieht man, wenn ein Plugin-Update etwas Neues einschleust.
Schritt 8 ist der, den man gern überspringt. Wer nach dem Aufräumen direkt scharf schaltet, verlässt sich darauf, dass alle Änderungen richtig waren. Der Berichtsmodus kann das prüfen, und er kostet nur Wartezeit.
Was nicht hilft#
Report-Only als Dauerzustand. Die Kopfzeile schützt nicht. Eine Seite, die seit einem Jahr nur beobachtet, hat keine CSP, sondern ein Protokoll.
Nur report-to, weil es das neue ist. Damit fehlen alle Berichte aus Browsern, die es noch nicht können — bei Firefox alles vor Version 149. Beide Direktiven setzen, bis die eigenen Zugriffszahlen zeigen, dass der alte Weg leer bleibt.
Berichte an eine http-Adresse. Reporting-Endpoints verwirft sie stillschweigend. Es kommt nichts an, und nichts sagt einem, warum.
Ein <meta>-Tag im Theme. Funktioniert für die scharfe Richtlinie, für die beobachtende nicht.
Erlauben, was gemeldet wird. Der Reflex, jede gemeldete Quelle in die Richtlinie zu schreiben, führt zu einer Liste von dreißig Hosts und 'unsafe-inline' obendrauf. Die Berichte zeigen, was aufzuräumen ist, nicht, was freizugeben ist. Welche fremden Skripte dabei den eigentlichen Befund liefern, steht in SEO-Risiken durch Third-Party-Skripte.
Die Berichte nach einem Nachmittag auswerten. Die Reporting-API liefert verzögert und gebündelt, und die Zugriffe auf selten besuchte Seiten kommen erst nach Tagen. Wer zu früh zählt, schaltet eine Richtlinie scharf, die eine Unterseite bricht, die noch niemand aufgerufen hatte.
Der ehrliche Schluss#
Der Berichtsmodus ist der einzige Weg, eine CSP auf einer laufenden Seite zu verschärfen, ohne sie abzuschießen. Er ist aber nur so gut wie die Stelle, an der die Berichte landen, und wie die Geduld, sie ein paar Tage liegen zu lassen. Was danach in der scharfen Richtlinie stehen soll und warum 'unsafe-inline' dort nichts verloren hat, steht in Content-Security-Policy: unsafe-inline loswerden. Welche Kopfzeilen neben der CSP noch fehlen und in welcher Reihenfolge man sie setzt, in Security-Header und SEO.
Kommentare
Gespeichert werden nur dein Name und dein Text — keine E-Mail-Adresse, keine IP-Adresse, kein Cookie. Jeder Kommentar wird vor der Veröffentlichung gelesen; das dauert meist einen Tag. Näheres in der Datenschutzerklärung.