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 deines Themes". Bitte nicht. Warum:
- Theme-Updates überschreiben
functions.phpstill und leise. Deine CSP verschwindet und du merkst 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 du entfernst 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: Starte mit 'unsafe-inline', stelle sicher dass der Rest deiner Policy sinnvoll ist (kein 'unsafe-eval', tight default-src, striktes frame-ancestors), und iteriere zu strikteren Policies später wenn du bestimmte Inline-Skripte auditiert hast.
'unsafe-inline' überhaupt wirklich nötig ist, basierend auf dem was deine Seite tatsächlich ausliefert. Das Ergebnis ist eine CSP, die direkt für deine exakte Theme-/Plugin-Kombination passt.
Typische Fehlerquellen
Die Admin-Bar taucht im Frontend auf und braucht eigene Origins
Wenn du eingeloggt bist, injiziert WordPress die Admin-Bar — die lädt inline Skripte, Styles und manchmal Gravatar-Bilder von externen Origins. Teste deine 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 deine CSP etwas, das gestern noch ging. Lass deine CSP nach größeren Plugin-Updates für eine Woche im Report-Only, oder abonniere report-uri-Benachrichtigungen damit du Verstöße siehst bevor Nutzer sich beschweren.
Gutenberg-Editor lädt nicht
Wenn du eine strikte CSP ohne 'unsafe-inline' in script-src durchsetzt, kann der Gutenberg-Block-Editor nicht mehr gerendert werden. Entweder lockerst du die Policy für /wp-admin/ (siehe der bedingte Ansatz unten) oder du behältst 'unsafe-inline'.
Bedingte CSP für /wp-admin/ vs. Frontend
Du kannst 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 setzt du 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 dein send_headers-Hook feuert überhaupt nicht. Wenn du so ein Plugin nutzt, deploye 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.