EN DE

Content Security Policy on WordPress

WordPress is the trickiest common platform to deploy a CSP on. Not because the code is hard — it's a 5-line hook — but because the WordPress ecosystem is full of inline scripts, styles and third-party widgets that the CSP has to whitelist. This guide covers the minimal deployment plus the real-world headaches: Gutenberg, plugin conflicts, admin bar, and where to put the code so a theme update doesn't wipe it out.

The minimal deployment

WordPress exposes a send_headers action hook that fires right before any response is sent. Register a callback that calls PHP's header():

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:");
});

That's the whole thing. The question is where to put it.

Where to put the code: must-use plugin beats functions.php

You'll see many tutorials say "add this to your theme's functions.php". Don't. Here's why:

The robust solution is a must-use plugin. Create the directory if it doesn't exist:

mkdir -p wp-content/mu-plugins

Then drop a file called wp-content/mu-plugins/csp.php with:

<?php
/**
 * Plugin Name: Content Security Policy
 * Description: Sets CSP header sitewide. Survives theme changes and 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 are loaded automatically — no "activate" step needed — and they're outside the theme/plugin update cycle, so they stay put forever unless you explicitly remove them.

Why WordPress almost always needs 'unsafe-inline'

WordPress core, almost every theme, and most plugins emit inline <script> and <style> blocks all over the page. A quick list of offenders:

Without 'unsafe-inline' in both script-src and style-src, the site breaks immediately. The stricter alternative (nonces) requires a WordPress CSP plugin that filters the output buffer and adds a nonce attribute to every inline tag. This is possible but rare in the wild — it's a non-trivial performance hit for every request.

Pragmatic recommendation: start with 'unsafe-inline', verify the rest of your policy is sensible (no 'unsafe-eval', tight default-src, strict frame-ancestors), and iterate to stricter policies later as specific inline scripts are audited.

Shortcut: our scanner crawls a WordPress site and detects exactly which external origins are loaded (fonts.googleapis.com, google-analytics.com, wp.com CDNs, cookie banners, etc.) plus flags whether 'unsafe-inline' is truly required based on what your site actually emits. You get a ready-to-paste CSP for your exact theme/plugin combination.

Common pitfalls

The admin bar shows on the frontend and needs its own origins

When you're logged in, WordPress injects the admin bar — which loads inline scripts, styles, and sometimes Gravatar images from external origins. Test your CSP both logged out AND logged in. The logged-in case typically needs extra whitelists for https://secure.gravatar.com in img-src.

Plugin conflicts on update

A plugin updates, starts loading a new external widget, and suddenly your CSP blocks something that worked yesterday. Keep your CSP in Report-Only for a week after major plugin updates, or subscribe to report-uri notifications so you see violations before users complain.

Gutenberg editor won't load

If you enforce a strict CSP without 'unsafe-inline' in script-src, the Gutenberg block editor will fail to render. Either loosen the policy for /wp-admin/ (see the conditional approach below) or keep 'unsafe-inline'.

Conditional CSP for /wp-admin/ vs. frontend

You can ship a strict CSP on the frontend while loosening it in the admin, where most of the inline-script pain lives:

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:");
    }
});

Note that send_headers may not fire on every admin request path — for maximum coverage, set the admin CSP in admin_init instead:

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 strip security headers

Some caching plugins (WP Rocket, W3 Total Cache, LiteSpeed Cache) serve static HTML files bypassing PHP entirely, which means your send_headers hook never fires. If you use such a plugin, deploy the CSP at the web-server level instead (Apache or Nginx) so cached responses still carry the header.

Inline scripts from theme customizer

The WordPress theme customizer writes inline CSS to <style> tags in the header. This requires 'unsafe-inline' in style-src. There is no clean workaround short of using a nonce-based approach.

Get a CSP for your exact WordPress setup → Detects your theme, plugins and every external widget automatically. No manual whitelisting.