Content Security Policy auf WordPress
WordPress ist die kniffligste der üblichen Plattformen wenn's um eine CSP geht. Nicht weil der Code schwierig wäre — es ist ein 5-Zeilen-Hook — sondern weil das WordPress-Ökosystem voll ist mit Inline-Skripten, Inline-Styles und Dritt-Anbieter-Widgets die die CSP alle whitelisten muss. Diese Anleitung deckt das minimale Deployment ab, plus die realen Kopfschmerzen: Gutenberg, Plugin-Konflikte, Admin-Bar und wo man den Code hinpackt, damit ein Theme-Update ihn nicht wegwischt.
Das minimale Deployment
WordPress bietet den send_headers-Action-Hook an, der unmittelbar vor dem Senden jedes Response feuert. Wir registrieren einen Callback der PHPs header() aufruft:
add_action('send_headers', function() {
header("Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:");
});Das ist alles. Die Frage ist, wo wir den Code ablegen.
Wo der Code hingehört: Must-Use-Plugin schlägt functions.php
Viele Tutorials sagen "pack das in die functions.php Ihres Themes". Bitte nicht. Warum:
- Theme-Updates überschreiben
functions.phpstill und leise. Ihre CSP verschwindet und Sie merken es erst wenn etwas bricht. - Theme-Wechsel entfernen die CSP komplett. Das ist ein überraschender Security-Rollback für etwas so Harmloses wie einen Theme-Swap.
- Child-Themes helfen (die Datei überlebt Parent-Theme-Updates) aber lösen das Theme-Wechsel-Problem nicht.
Die robuste Lösung ist ein Must-Use-Plugin. Ordner anlegen falls er nicht existiert:
mkdir -p wp-content/mu-pluginsUnd dann eine Datei wp-content/mu-plugins/csp.php mit:
<?php
/**
* Plugin Name: Content Security Policy
* Description: Setzt den CSP-Header seitenweit. Überlebt Theme-Wechsel und Updates.
*/
if (!defined('ABSPATH')) exit;
add_action('send_headers', function() {
header("Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:");
});Must-Use-Plugins werden automatisch geladen — kein "Aktivieren"-Schritt nötig — und sie sind außerhalb des Theme-/Plugin-Update-Zyklus. Sie bleiben also für immer bestehen, außer Sie entfernen sie explizit.
Warum WordPress fast immer 'unsafe-inline' braucht
WordPress-Core, fast jedes Theme und die meisten Plugins erzeugen überall auf der Seite Inline-<script>- und <style>-Blöcke. Eine kurze Liste der Täter:
- Core injiziert inline-Übersetzungen via
wp-i18n, inlinetinymce-Config im Admin und inline jQuery-Trigger - Gutenberg (der Block-Editor) basiert stark auf inline React-Bootstrap-Skripten
- Plugins wie WooCommerce, Contact Form 7, Yoast SEO, Akismet injizieren alle inline JavaScript
- Themes erzeugen inline CSS Custom Properties für den Customizer
Ohne 'unsafe-inline' in sowohl script-src als auch style-src bricht die Seite sofort. Die striktere Alternative sind Nonces: ein Zufallswert, der bei jedem Seitenaufruf neu erzeugt wird, im Header steht und an jedem erlaubten <script>-Tag wiederholt wird. Was keinen passenden Wert trägt, führt der Browser nicht aus.
Pragmatische Empfehlung: Starten Sie mit 'unsafe-inline', stellen Sie sicher dass der Rest Ihrer Policy sinnvoll ist (kein 'unsafe-eval', tight default-src, striktes frame-ancestors), und iterieren Sie zu strikteren Policies später wenn Sie bestimmte Inline-Skripte auditiert haben.
Braucht man dafür ein Plugin?
Wenn Sie nur einen festen Header setzen wollen, nein — die fünf Zeilen oben tun das, und ein Plugin würde dieselbe Aufgabe mit mehr beweglichen Teilen erledigen. Interessant werden Plugins erst bei Nonces, denn die lassen sich nicht in einer einzelnen header()-Zeile unterbringen: Jeder Aufruf braucht einen neuen Wert, und der muss an jedes einzelne Skript-Tag der Seite gelangen.
Strict CSP: das Plugin, das es richtig macht
Das Plugin Strict CSP löst das über WP_Scripts — also über die Kern-Schnittstelle, mit der Themes und Plugins ihre Skripte ohnehin registrieren sollen. Möglich wurde das mit Trac #58664, das die letzten Stellen beseitigte, an denen der Core seine <script>-Tags von Hand zusammenbaute.
Was diese Bauweise praktisch bedeutet:
- Kein Output-Buffering. Die Nonce entsteht bei der Registrierung des Skripts, nicht durch nachträgliches Durchsuchen des fertigen HTML. Das ist der saubere Weg — und der, der nichts kostet.
- Frontend und Login-Seite, noch nicht
wp-admin. Der Admin-Bereich bleibt vorerst außen vor (#59446). Für das Frontend, wo Ihre Besucher sind, reicht das. - Es schützt nur, was sich an die Regeln hält. Wer ein
<script>direkt perechoausgibt, stattwp_add_inline_script()zu verwenden, bekommt keine Nonce — und sein Skript wird blockiert. Das betrifft nicht nur fremde Plugins: Einige mitgelieferte Core-Themes machen es bis heute falsch (#63806).
Der letzte Punkt ist der ehrliche Haken. Strict CSP macht eine Seite nicht strict-CSP-fähig — es zeigt Ihnen, ob sie es schon ist. Rechnen Sie damit, dass das eine oder andere Plugin nach der Aktivierung nicht mehr funktioniert. Genau deshalb gehört auch dieser Schritt zuerst in den Report-Only-Modus.
Was ein Plugin kostet
Plugins, die stattdessen das fertige HTML nachträglich umschreiben — der Weg über das Output-Buffering — bezahlen das bei jedem einzelnen Request. Schlimmer noch ist das Zusammenspiel mit Page-Caching: Eine gecachte Seite behält die Nonce, mit der sie erzeugt wurde, während der Header bei jedem Aufruf eine frische mitschickt. Die beiden passen nicht mehr zusammen, und die Policy blockiert ihre eigenen Skripte. Wenn ein Plugin mit Nonce-Unterstützung wirbt, lohnt sich deshalb die Frage, welchen der beiden Wege es geht.
Unabhängig davon ist jedes Plugin eine weitere Sache, die aktualisiert wird, bricht oder irgendwann nicht mehr gepflegt wird. Ein Security-Header, der nach einem Update stillschweigend nicht mehr gesendet wird, ist schlechter als einer, der nie gesetzt war — weil es niemandem auffällt.
Wann ein Plugin die richtige Wahl ist
Wenn Sie wirklich eine Nonce-basierte CSP brauchen — bei einer Seite mit Zahlungsverkehr oder personenbezogenen Daten, wo 'unsafe-inline' nicht vertretbar ist — dann ist Strict CSP der vernünftigste Ausgangspunkt, sofern Ihre Themes und Plugins ihre Skripte so registrieren, wie WordPress es vorsieht. Planen Sie Zeit dafür ein, die zu finden, die es nicht tun.
Für alles andere gilt: Das Must-Use-Plugin aus dem vorigen Abschnitt sendet denselben Header ohne jeden Overhead, lässt sich nicht versehentlich deaktivieren und überlebt jeden Theme-Wechsel.
'unsafe-inline' überhaupt wirklich nötig ist, basierend auf dem was Ihre Seite tatsächlich ausliefert. Das Ergebnis ist eine CSP, die direkt für Ihre exakte Theme-/Plugin-Kombination passt.Typische Fehlerquellen
Die Admin-Bar taucht im Frontend auf und braucht eigene Origins
Wenn Sie eingeloggt sind, injiziert WordPress die Admin-Bar — die lädt inline Skripte, Styles und manchmal Gravatar-Bilder von externen Origins. Testen Sie Ihre CSP sowohl ausgeloggt ALS AUCH eingeloggt. Der eingeloggte Fall braucht typischerweise extra Freigaben für https://secure.gravatar.com in img-src.
Plugin-Konflikte bei Updates
Ein Plugin updated, fängt an ein neues externes Widget zu laden, und plötzlich blockiert Ihre CSP etwas, das gestern noch ging. Lassen Sie Ihre CSP nach größeren Plugin-Updates für eine Woche im Report-Only, oder abonnieren Sie report-uri-Benachrichtigungen damit Sie Verstöße sehen bevor Nutzer sich beschweren.
Gutenberg-Editor lädt nicht
Wenn Sie eine strikte CSP ohne 'unsafe-inline' in script-src durchsetzen, kann der Gutenberg-Block-Editor nicht mehr gerendert werden. Entweder lockern Sie die Policy für /wp-admin/ (siehe der bedingte Ansatz unten) oder behalten 'unsafe-inline' bei.
Bedingte CSP für /wp-admin/ vs. Frontend
Sie können im Frontend eine strikte CSP ausliefern und im Admin — wo die meisten Inline-Schmerzen wohnen — lockerer sein:
add_action('send_headers', function() {
if (is_admin()) {
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:");
} else {
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:");
}
});Zu beachten: send_headers feuert nicht auf jedem Admin-Request-Pfad. Für maximale Abdeckung setzen Sie die Admin-CSP stattdessen in admin_init:
add_action('admin_init', function() {
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:");
});Caching-Plugins entfernen Security-Header
Manche Caching-Plugins (WP Rocket, W3 Total Cache, LiteSpeed Cache) liefern statische HTML-Dateien aus und umgehen PHP komplett — das bedeutet Ihr send_headers-Hook feuert überhaupt nicht. Wenn Sie so ein Plugin nutzen, deployen Sie die CSP besser auf Webserver-Ebene (Apache oder Nginx), damit gecachte Responses den Header trotzdem tragen.
Inline-Skripte vom Theme-Customizer
Der WordPress-Theme-Customizer schreibt Inline-CSS in <style>-Tags im Header. Das braucht 'unsafe-inline' in style-src. Ohne einen nonce-basierten Ansatz gibt's keinen sauberen Workaround.