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-plugins
Und 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 (Nonces) erfordert ein WordPress-CSP-Plugin das den Output-Buffer filtert und jedem Inline-Tag ein Nonce-Attribut verpasst. Das ist machbar aber in freier Wildbahn selten — ein nicht-trivialer Performance-Overhead auf jeden Request.
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.
'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.