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:

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:

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:

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.

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 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.

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