Content Security Policy in nginx: CSP richtig setzen – mit PairDrop und SkySend
HSTS, nosniff, Referrer-Policy – die Grundlagen der Sicherheits-Header haben wir in
Teil 2 der nginx-Serie gesetzt. Ein Header fehlt dort
systematisch, weil er sich nicht „mal eben“ dazuschreiben lässt: die
Content Security Policy. Sie ist der einzige Header, der nicht nur transportiert, sondern
im Browser entscheidet, was eine Seite laden und ausführen darf. Dieser Artikel erklärt sie
ausführlich, zeigt zwei fertige Policies für selbst gehostete Dienste (PairDrop
und SkySend) und den Weg dorthin, der keine Besucher aussperrt.
Was CSP leistet – und was nicht
Eine Content Security Policy ist eine Erlaubnisliste, die der Server mit jeder Antwort verschickt. Sie legt fest, von welchen Quellen der Browser Skripte, Stile, Bilder, Schriften und Verbindungen akzeptiert. Der Nutzen entsteht nicht dadurch, dass deine Seite unsicher wäre – sondern dadurch, dass eine Einschleusung dann nicht mehr ausgenutzt werden kann.
| Angriff | Ohne CSP | Mit CSP |
|---|---|---|
| Eingeschleustes Fremdskript | Läuft im Kontext deiner Seite, liest Cookies und Sitzungen | Wird blockiert, wenn die Quelle nicht erlaubt ist – oder wenn Nonce/Hash fehlt |
Injizierter <script>-Block im HTML | Führt direkt aus | Wird blockiert, sobald kein 'unsafe-inline' erlaubt ist |
| Clickjacking (Seite in fremdem Frame) | Angreifer legt unsichtbare Klickflächen darüber | frame-ancestors 'none' verhindert die Einbettung |
Datenabfluss über fetch/WebSocket | Beliebige Ziele erreichbar | connect-src grenzt die Ziele ein |
| Umleitung von Formularen | Formular postet an einen Fremdhost | form-action 'self' verhindert das |
<base>-Tag-Trick | Alle relativen Pfade zeigen plötzlich auf den Angreifer | base-uri 'none' unterbindet das |
nosniff und keine
Backups. Sie ist das Sicherheitsnetz für den Fall, dass etwas durchkommt.
Header ja, Meta nur eingeschränkt
Man kann eine Policy auch per <meta http-equiv="Content-Security-Policy"> in die Seite
schreiben. Das ist bei statischen Seiten populär, weil man keine Serverkonfiguration braucht – aber es
fehlen die wichtigen Teile:
| Thema | HTTP-Header | <meta>-Variante |
|---|---|---|
frame-ancestors | Wirkt | Wird ignoriert – Einbettungsschutz nur über den Header |
report-uri / report-to | Wirkt | Wird ignoriert |
| Report-Only-Modus | Vollständig verfügbar | Nicht möglich – es gibt nur die scharfe Fassung |
| Erweiterbarkeit | Policies lassen sich stapeln (mehrere Header) | Eine Policy, keine Erweiterung möglich |
| Änderbarkeit | Zentral im Proxy für alle Dienste | Pro App im Template – bei Fremd-Apps praktisch nicht machbar |
Deshalb gehört CSP in nginx. Zwei Regeln vorab, die man sonst schmerzhaft lernt:
- Die Policy gilt pro Antwort, nicht pro Domain. Verschiedene Apps auf derselben Domain brauchen unterschiedliche Policies – das ist eine Architekturfrage, kein Konfigurationsdetail.
- Genau ein CSP-Header pro Antwort. Bei zwei Headern setzt der Browser beide durch, also die strengere – und du suchst eine Weile, warum die „neue“ Policy die Seite zerschießt.
Die Direktiven in der Praxis
Eine Policy ist eine Folge von direktive quelle quelle, getrennt durch Semikolon.
default-src ist der Auffangwert für alles, was nicht einzeln genannt wird. In der Praxis
sind es diese hier:
| Direktive | Steuert | Was bricht, wenn sie zu eng ist |
|---|---|---|
default-src 'self' | Alles, was nicht extra aufgeführt ist | Sehr viel – deshalb möglichst gezielt überschreiben |
script-src | JavaScript-Dateien und Inline-Skripte | Die App startet nicht mehr: leere Seite oder eingefrorene Oberfläche |
style-src | CSS-Dateien und Inline-Stile | Layout kaputt – typischer Fall für 'unsafe-inline' |
img-src | Bilder, Icons, Vorschaubilder | Fehlende Bilder – bei Apps mit Canvas-Vorschau braucht es blob: und data: |
font-src | Schriftdateien | Falsche Schriftart (Fallback) oder gar keine Anzeige bei Icon-Fonts |
connect-src | fetch, XHR, WebSocket, EventSource | Uploads, Statusmeldungen und Live-Updates brechen ab |
media-src | Audio und Video | Wiedergabe startet nicht, Vorschau bleibt schwarz |
worker-src | Web- und Service-Worker | PWA-Funktionen, Offline-Modus, Upload-Worker fallen aus |
manifest-src | Web-App-Manifest | Installierbarkeit, Icon und Name der PWA fehlen |
frame-src | Eingebettete iframes | Einbettungen (Karten, Videos) bleiben leer |
object-src 'none' | Plugins, Flash-Erben, Embed-Objekte | Praktisch nichts – gehört immer auf 'none' |
base-uri 'none' | <base>-Tag | Nichts, außer die Seite nutzt bewusst ein Basis-Tag |
form-action 'self' | Ziel von Formular-Absendungen | Login- oder Suchformulare schlagen fehl, wenn sie extern posten |
frame-ancestors 'none' | Wer deine Seite einbetten darf | Einbettung in fremde Seiten – gewollt bei Widgets |
upgrade-insecure-requests | Anheben von http:// auf https:// | Alte http-Ressourcen laden nicht mehr – in der Regel gewünscht |
default-src 'self'; img-src 'self' data: blob:; object-src 'none'; base-uri 'none';
frame-ancestors 'none'; form-action 'self'; upgrade-insecure-requests – und dann nur das
freigeben, was die App nachweislich braucht: connect-src für WebSockets,
worker-src für Worker, media-src für Medien.
Die drei, die nie etwas kaputtmachen
Wenn du Zeit für nichts anderes hast, setze diese drei – sie kosten keine Funktion und schließen drei Angriffsklassen:
add_header Content-Security-Policy "object-src 'none'; base-uri 'none'; frame-ancestors 'none'" always;
| Direktive | Verhindert | Risiko |
|---|---|---|
object-src 'none' | Einbetten aktiver Inhalte über Plugin-Schnittstellen | Keins – moderne Apps nutzen das nicht |
base-uri 'none' | Umleiten aller relativen Pfade über ein injiziertes <base> | Keins, solange die App kein eigenes Basis-Tag braucht |
frame-ancestors 'none' | Clickjacking durch Einbettung | Nur, wenn du selbst einbettest – dann 'self' statt 'none' |
X-Frame-Options: frame-ancestors ist der moderne Weg
und kann mehr (mehrere erlaubte Eltern, Wildcards). Du darfst beide Header parallel senden – alte Clients
nehmen den einen, neue den anderen.
'unsafe-inline', Hashes, Nonces, strict-dynamic
Der häufigste Grund, warum eine Policy „nicht geht“, sind Inline-Skripte und Inline-Stile. Ohne Erlaubnis
blockiert der Browser jeden <script>-Block und jedes style="…"-Attribut.
Drei Wege führen daraus – mit sehr unterschiedlichem Aufwand:
| Weg | Wie es funktioniert | Aufwand und Grenzen |
|---|---|---|
'unsafe-inline' | Inline-Code generell erlaubt | Null Aufwand, aber der Schutz gegen eingeschleusten Inline-Code entfällt |
| Hash | Der SHA-256-Wert jedes erlaubten Inline-Blocks steht in der Policy | Sehr strikt – aber jede Änderung an der App ändert den Hash und bricht die Seite |
| Nonce | Ein Zufallswert pro Antwort wird in Policy und Skript-Tag gesetzt | Stark, aber die App muss den Nonce in ihre Tags schreiben – bei Fremd-Apps oft nicht möglich |
'strict-dynamic' | Nur mit Nonce/Hash: erlaubte Skripte dürfen weitere laden; Host-Listen werden ignoriert | Der modernste Weg – setzt Nonce oder Hash voraus |
Für selbst gehostete Fremd-Apps (also der Normalfall bei PairDrop und SkySend) läuft es praktisch auf
'unsafe-inline' für Stile hinaus, während Skripte meist strikt bleiben können.
Das ist ein bewusster Kompromiss: Stil-Injection ist deutlich harmloser als Skript-Injection.
$request_id kannst du
einen Zufallswert erzeugen, ihn in den Header schreiben und über sub_filter in die
HTML-Antwort einsetzen. Das funktioniert nur bei Inhalten, die du selbst ausliefern kannst, verlangt
zusätzliche Konfiguration (Kompression aus, sub_filter_once off) und bricht, sobald die App
eigene Skript-Tags dynamisch erzeugt. Für eigene Templates ein Gewinn – für fremde Apps Rate- und
Fehlerquelle.
Der sichere Weg: erst Report-Only
Der einzige Weg, eine CSP einzuführen, ohne Besucher auszusperren: erst beobachten, dann
durchsetzen. Dafür gibt es denselben Header mit angehängtem -Report-Only – er
blockiert nichts, sondern meldet nur.
# Schritt 1: beobachten (blockiert nichts)
add_header Content-Security-Policy-Report-Only "default-src 'self'; img-src 'self' data: blob:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'" always;
# Schritt 2: nach der Auswertung scharf schalten
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data: blob:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'" always;
Der wichtigste Trick kostet nichts: die Konsole des Browsers ist dein Report-Endpunkt. Bei jedem Verstoß erscheint dort eine Meldung mit der verletzten Direktive und der blockierten Adresse. Öffne die Seite, klicke dich durch die Funktionen – hochladen, teilen, Vorschau, PWA installieren – und lies die Ausgabe. Nach zehn Minuten weißt du mehr über die App als jede Dokumentation verrät.
report-uri ist völlig
legitim. Der Browser loggt trotzdem – und du vermeidest, dass deine Besucher Meldungen an irgendeinen
Server schicken. Für einen ersten Durchlauf ist das der datenschutzfreundlichste Weg.
Reports sammeln ohne Drittanbieter
Wenn du Reports zentral auswerten willst, kannst du das selbst bauen. Zwei Wege:
# Variante A: Browser senden an den eigenen Endpunkt
add_header Content-Security-Policy-Report-Only "…; report-uri /csp-report" always;
# Variante B (moderner, ersetzt report-uri): Reporting-API
add_header Report-To '{"group":"csp","max_age":86400,"endpoints":[{"url":"https://pd.huuu.biz/csp-report"}]}' always;
# und in der Policy: report-to csp
Für den Endpunkt habe ich eine naheliegende Idee getestet, die nicht funktioniert: Eine
reine nginx-Lösung mit return 204; und $request_body im Log. Das Ergebnis:
HTTP 204 kommt an, das Log enthält aber nur einen Bindestrich – nginx liest den Body bei einem
return nicht, also ist $request_body leer. Wenn du die Reports wirklich
speichern willst, brauchst du ein Backend davor:
location = /csp-report {
proxy_pass http://127.0.0.1:8787; # kleiner Sammler, siehe Skript unten
client_max_body_size 32k; # Reports sind klein – Bremse gegen Missbrauch
access_log off;
}
Der Sammler ist ein Dutzend Zeilen: Body lesen, als JSON-Zeile anhängen, mit 204 antworten. Genau in dieser Form läuft er im Test – der Browser bekommt sein 204, in der Logdatei steht der vollständige Report:
#!/usr/bin/env node
// csp-collector.mjs - sammelt CSP-Reports in eine JSONL-Datei
import http from "node:http";
import fs from "node:fs";
const PORT = Number(process.env.PORT || 8787);
const LOG = process.env.LOG || "/var/log/csp-reports.jsonl";
http.createServer((req, res) => {
if (req.method !== "POST" || !req.url.startsWith("/csp-report")) {
res.writeHead(404); return res.end();
}
let body = "";
req.on("data", (c) => { body += c; if (body.length > 64 * 1024) req.destroy(); });
req.on("end", () => {
fs.appendFileSync(LOG, JSON.stringify({ zeit: new Date().toISOString(), ip: req.socket.remoteAddress, rohdaten: body.slice(0, 8000) }) + "\n");
res.writeHead(204); res.end(); // 204 genuegt dem Browser
});
}).listen(PORT, "127.0.0.1", () => console.log("Collector auf 127.0.0.1:" + PORT));
Mit socat, einem systemd-Unit und einer Logrotation läuft das dauerhaft. Wer es „richtig“
will, nimmt einen selbst gehosteten Fehlersammler (etwa GlitchTip oder Sentry) und zeigt dorthin – dann
hast du Gruppierung und Auswertung, aber wieder einen Dienst mehr zu warten.
Praxis 1: PairDrop absichern
PairDrop ist eine WebRTC-Anwendung: Zwei Geräte finden sich über einen Vermittlungsserver und übertragen Daten anschließend direkt miteinander. Für die Policy heißt das:
| Was die App braucht | Direktive |
|---|---|
| Signalisierung per WebSocket auf derselben Domain | connect-src 'self' |
| Vorschaubilder, die im Browser entstehen (Canvas, Blob-Links) | img-src 'self' data: blob: |
| Vorschau von Audio/Video vor dem Senden | media-src 'self' blob: |
| Als installierbare Web-App mit Offline-Fähigkeit | worker-src 'self' blob: und manifest-src 'self' |
| Dynamisch aufgebaute Oberfläche (Stile) | style-src 'self' 'unsafe-inline' |
| Einbettung in fremde Seiten | frame-ancestors 'none' – bei einem Transferdienst unerwünscht |
Daraus entsteht die Policy für pd.huuu.biz beziehungsweise deine eigene Instanz:
# Einmalig im http-Kontext definieren (z. B. conf.d/websocket-upgrade.conf):
map $http_upgrade $connection_upgrade { default upgrade; '' close; }
server {
listen 443 ssl;
http2 on;
server_name pd.meine.domain;
ssl_certificate /etc/ssl/private/pd.meine.domain_ecc/fullchain.cer;
ssl_certificate_key /etc/ssl/private/pd.meine.domain_ecc/pd.meine.domain.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob:; font-src 'self'; connect-src 'self'; worker-src 'self' blob:; manifest-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests" always;
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # WebSocket-Signalisierung
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:50513;
}
}
connect-src 'self' stun: stun.example.org. Das ist wirkungslos: CSP kann die
Peer-to-Peer-Verbindungen von WebRTC nicht einschränken. Erlaubt werden muss nur die
Signalisierung – und die läuft bei PairDrop über WebSocket auf der eigenen Domain, also
'self'. Wer eigene STUN/TURN-Server nutzt, trägt sie in rtc_config.json ein
(siehe PairDrop-Artikel), nicht in der CSP.
blob: oder worker-src.
③ Erst dann -Report-Only aus dem Header entfernen und einmal nginx -t && systemctl reload nginx.
Praxis 2: SkySend absichern
SkySend ist der zweite gute Testfall, weil es anders funktioniert:
Die Dateien werden im Browser verschlüsselt, der Schlüssel steckt im URL-Fragment, und die Uploads laufen
laut Konfiguration über FILE_UPLOAD_WS per WebSocket. Genau diese Punkte muss die Policy
abbilden.
| Was die App braucht | Direktive | Warum |
|---|---|---|
| Uploads per WebSocket | connect-src 'self' | Gleiche Domain, gleiche Herkunft – der Upload-Kanal läuft über das Fragment-freie Pfadsystem |
| Verschlüsselung im Browser | script-src 'self' | Kein Fremd-CDN, keine Inline-Skripte nötig |
| Fortschrittsanzeige und Ausschnitte | img-src 'self' data: blob: | Vorschauen entstehen lokal als Blob |
| Große Uploads über Worker | worker-src 'self' blob: | Wenn die App in Worker auslagert, sonst bricht der Upload ab |
| Installierbare Web-App | manifest-src 'self' | Icon und Name der PWA |
| Einbettung | frame-ancestors 'none' | Ein Teilen-Dienst gehört nicht in fremde Frames |
Die fertige Policy für sky.huuu.biz beziehungsweise deine Instanz:
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob:; font-src 'self'; connect-src 'self'; worker-src 'self' blob:; manifest-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests" always;
| Besonderheit | Was du tun solltest |
|---|---|
'wasm-unsafe-eval' | Nur nötig, wenn die App WebAssembly für die Verschlüsselung nutzt. Im Report-Only-Lauf siehst du eine Meldung, falls es fehlt („WebAssembly compilation blocked“) – dann Token aufnehmen, sonst weglassen. |
Referrer-Policy | Bleibt auf no-referrer wie im SkySend-Artikel: Der Schlüssel im URL-Fragment darf nirgends landen. |
| Sehr große Uploads | client_max_body_size 0; beibehalten – das Limit prüft die App selbst. Die CSP ist davon unberührt. |
| Download-Links | Kein CSP-Thema: Downloads sind Navigation, keine Ressource. form-action und img-src reichen. |
connect-src-Eintrag erst beim tatsächlichen Hochladen auf – die Seite sieht vorher
vollständig in Ordnung aus. Lade also im Report-Only-Lauf eine große Datei hoch und beobachte die Konsole.
nginx-Fallen beim Setzen der Header
| Falle | Verhalten | Lösung |
|---|---|---|
add_header erbt nicht | Sobald in einer location ein eigener add_header steht, gelten die Header der Server-Ebene dort nicht mehr | Sicherheits-Header in Snippets auslagern und in jede Ebene per include einbinden |
Kein always | Bei 4xx/5xx-Antworten fehlen die Header – genau dort sind sie interessant | Immer mit always setzen |
| Zwei CSP-Header | Der Browser setzt beide durch (Schnittmenge) – die strengere gewinnt | Genau eine Policy pro Antwort, auch über Snippets sicherstellen |
| Zeilenumbruch im Wert | Getestet: nginx akzeptiert einen mehrzeiligen Wert und schickt ihn als eine Zeile mit Leerzeichen | Lesbar ist es – üblich bleibt die einzeilige Schreibweise, weil sie beim Kopieren keine Überraschungen macht |
| Leerzeichen vor dem Semikolon | Kein Problem – die Policy bleibt gültig | Kosmetik, kein Grund zur Sorge |
# /etc/nginx/snippets/security-headers.conf - gemeinsame Basis
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# /etc/nginx/snippets/csp-pairdrop.conf - app-spezifisch
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; …" always;
# im Serverblock
include snippets/security-headers.conf;
include snippets/csp-pairdrop.conf;
Mehrere Dienste, eine Domain
Sobald zwei Apps dieselbe Domain teilen, brauchst du die CSP pro location – und läufst in
die Vererbungsregel. Sauber geht es so:
| Aufbau | CSP | Bewertung |
|---|---|---|
| Eine Subdomain pro App (Empfehlung) | Ein eigener Serverblock mit eigener Policy | Klar, wartbar, keine Vererbungsfallen – so arbeiten auch unsere Demo-Instanzen |
| Mehrere Apps unter einer Domain per Pfad | Pro location mit eigenem include | Möglich, aber jeder Pfad braucht die vollständige Header-Liste; schnell unübersichtlich |
| Eine Policy für alles | Serverweit | Nur sinnvoll, wenn alle Apps dieselben Bedürfnisse haben – praktisch selten |
Testen, ohne Besucher zu verlieren
# 1) Ist der Header ueberhaupt gesetzt?
curl -sI https://pd.huuu.biz/ | grep -i 'content-security'
# 2) Scharf oder nur im Testmodus?
curl -sI https://pd.huuu.biz/ | grep -io 'content-security-policy\(-report-only\)\?'
# 3) Konfiguration vor dem Reload pruefen
sudo nginx -t && sudo systemctl reload nginx
| Prüfung | Wie | Aussage |
|---|---|---|
| Grundzustand | Seite laden, Konsole öffnen, Fehlerliste leer? | Zeigt sofort, ob die Policy zu streng ist |
| Alle Funktionen durchklicken | Hochladen, teilen, Vorschau, Einstellungen, PWA installieren | Der wichtigste Test überhaupt – nur hier fallen connect-src und worker-src auf |
| Angriff simulieren | In einer Testseite ein Skript von einem Fremdhost einbinden | Wird es blockiert, wirkt script-src – inklusive Meldung in der Konsole |
| Einbettung testen | Die Seite in einem iframe auf einer anderen Domain laden | Bleibt sie leer, greift frame-ancestors |
| Report-Only-Phase | Mehrere Tage mit echten Nutzern laufen lassen | Deckt Wege auf, die du selbst nie klickst |
'wasm-unsafe-eval' in älteren Versionen) und meldet anders –
wenn eine Funktion nur in einem Browser bricht, ist das die Erklärung.
Wenn es schiefgeht
| Symptom | Ursache | Vorgehen |
|---|---|---|
| Komplett weiße Seite | Skripte blockiert | Konsole lesen, script-src prüfen – im Zweifel kurzfristig Report-Only zurück |
| Layout zerschossen, Funktionen gehen | Inline-Stile blockiert | style-src 'self' 'unsafe-inline' ergänzen |
| Bilder oder Vorschauen fehlen | blob: bzw. data: fehlt in img-src | Beide Quellen aufnehmen – sie sind bei lokalen Vorschauen unvermeidlich |
| Upload bricht ab, Seite wirkt okay | WebSocket von connect-src blockiert | connect-src 'self' setzen, Upload erneut testen |
| „Refused to connect“ in der Konsole | Fremd-API wird angesprochen | Entweder freigeben oder – besser – prüfen, warum die App überhaupt fremd lädt |
| PWA lässt sich nicht installieren | manifest-src oder worker-src fehlt | Beide auf 'self' setzen, Worker zusätzlich blob: |
| Nach dem Update plötzlich kaputt | Die App nutzt jetzt einen neuen Host oder Inline-Code | Reports/Reports-Only-Zeitraum nutzen, Policy nachziehen – Version im Kommentar notieren |
| Nur ein Browser betroffen | Ungleiche Token-Unterstützung | Token-Unterschiede prüfen, konservative Schreibweise wählen |
'unsafe-inline' plus * – damit ist die Policy wertlos. Besser: Report-Only
wieder einschalten, die fehlende Quelle in der Konsole suchen und genau diese eine ergänzen.
Häufige Fragen (FAQ)
| Frage | Antwort |
|---|---|
| Bricht CSP meine Seite? | Nur in der scharfen Variante und nur, wenn die Policy etwas blockiert, das die App braucht. Im Report-Only-Modus passiert nichts. |
| Bringt CSP etwas ohne XSS-Lücke? | Ja – sie schützt für den Fall, dass eine entsteht. Das ist derselbe Gedanke wie bei Backups: Vorbereitung für den unwahrscheinlichen Fall. |
Kann ich 'unsafe-inline' ganz vermeiden? | Bei eigenen Templates ja (Nonce oder Hash). Bei fremden Anwendungen meist nicht – dann bleibt es ein bewusster Kompromiss für Stile. |
| Warum sehe ich zwei CSP-Header? | Weil Server- und Location-Ebene je einen senden. Der Browser setzt beide durch. Entferne einen davon, sonst sind Fehlersuchen vorprogrammiert. |
Ersetzt CSP X-Frame-Options? | Für moderne Browser ja. Beide parallel zu senden ist üblich und schadet nicht. |
| Ist CSP ein Rankingfaktor? | Nein. Sie ist ein Sicherheitsmerkmal, keine SEO-Maßnahme. |
| Muss ich Reports speichern? | Nein. Für Hobby-Setups reicht die Konsole. Sobald du Reports sammelst, sind es Nutzerdaten – dann gehören Aufbewahrung und Datenschutzerklärung dazu. |
| Gilt die Policy auch für HTTP/3? | Ja. Header sind transportunabhängig – die Policy wird bei HTTP/1.1, HTTP/2 und HTTP/3 gleichermaßen beachtet. |
Fazit
CSP ist der Header mit der größten Wirkung und dem größten Selbsterschütterungspotenzial – je nachdem, in welcher Reihenfolge man ihn einführt. Mit Report-Only, der Konsole als Report-Endpunkt und einer Policy, die pro Anwendung gebaut wird, ist es dagegen ein ruhiger Vorgang: beobachten, ergänzen, scharf schalten.
PairDrop und SkySend zeigen dabei die zwei Muster, die dir bei jedem selbst gehosteten Dienst begegnen:
eine App mit WebRTC, bei der die Peer-Verbindungen außerhalb der CSP liegen, und eine App mit
WebSocket-Uploads, bei der ein fehlender connect-src-Eintrag erst beim echten Transfer
auffällt.
<meta> – sonst fehlen frame-ancestors und Reports.
② Immer erst -Report-Only, Funktionen durchklicken, dann scharf schalten.
③ object-src, base-uri und frame-ancestors kosten nichts und schließen drei Angriffsklassen.
④ Genau eine Policy pro Antwort – add_header vererbt nicht, Snippets verhindern den Salat.
⑤ WebRTC kann CSP nicht einschränken; WebSocket-Uploads dagegen schon – deshalb connect-src immer mit dem echten Upload testen.