EN DE

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:

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:

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.

Abkürzung: Unser Scanner crawlt eine WordPress-Seite und erkennt, welche externen Origins geladen werden (fonts.googleapis.com, google-analytics.com, wp.com-CDNs, Cookie-Banner etc.) — plus markiert ob '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.

Eine CSP für dein genaues WordPress-Setup → Erkennt dein Theme, deine Plugins und jedes externe Widget automatisch. Kein manuelles Freigeben.