WordPress and Content Security Policy have a complicated relationship. WordPress was built in an era before CSP existed, and it shows. The admin panel injects inline scripts constantly. Plugins do whatever they want. Themes include jQuery from who-knows-where.

But here’s the thing: your WordPress site is a target. It powers 43% of the web, which means attackers have spent years finding ways to exploit it. Adding CSP is one of the most impactful things you can do to protect it.

Let’s walk through how to actually make this work.

The Core Problem

WordPress has two distinct parts that need different policies:

  1. The frontend — What visitors see. This is where CSP is most valuable and easiest to implement.
  2. The admin panel — Where you manage content. This is a CSP nightmare.

The admin panel uses inline scripts extensively. The Block Editor (Gutenberg) generates inline JavaScript. The Customizer, media uploader, and various admin screens all inject scripts dynamically. Trying to apply a strict CSP to wp-admin is an exercise in frustration.

So here’s my recommendation: use a strict policy on the frontend and a permissive policy on the admin. It’s pragmatic and it works.

The Frontend Policy

Add this to your theme’s functions.php or a custom plugin:

function my_csp_headers() {
    // Skip admin — we'll handle that separately
    if (is_admin() || is_user_logged_in()) {
        return;
    }
    
    header("Content-Security-Policy: "
        . "default-src 'self'; "
        . "script-src 'self' https://www.googletagmanager.com; "
        . "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "
        . "img-src 'self' data: https: https://i0.wp.com https://i1.wp.com https://i2.wp.com https://s.w.org; "
        . "font-src 'self' https://fonts.gstatic.com; "
        . "connect-src 'self' https://www.google-analytics.com; "
        . "frame-ancestors 'none'; "
        . "base-uri 'self'; "
        . "form-action 'self'");
}
add_action('send_headers', 'my_csp_headers');

Let me break down what’s going on here:

The is_admin() check — This ensures the policy only applies to frontend pages. Your admin panel won’t be affected.

img-src includes WordPress CDN domainsi0.wp.com, i1.wp.com, i2.wp.com are WordPress.com’s CDN, used for Jetpack Photon and gravatar images. If you use Jetpack or any plugin that serves images through WordPress.com’s infrastructure, you need these. s.w.org is for WordPress emoji images.

style-src ‘unsafe-inline’ — Yes, this is a compromise. Many WordPress themes and page builders inject inline styles. Gutenberg itself adds inline styles for block formatting. You could fight this, but you’d be fighting your entire ecosystem. Accept the compromise and move on.

The emoji problem — WordPress injects emoji JavaScript by default. If you don’t need emojis (you probably don’t on a professional site), disable them entirely:

remove_action('wp_head', 'print_emoji_detection_script', 7);
remove_action('wp_print_styles', 'print_emoji_styles');

This removes the emoji script injection and saves you a CSP headache.

The Admin Policy

For the admin panel, be permissive but still get some value:

function my_admin_csp_headers() {
    if (!is_admin()) {
        return;
    }
    
    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'; "
        . "connect-src 'self'; "
        . "frame-src 'self'");
}
add_action('send_headers', 'my_admin_csp_headers');

This is weak from a script perspective, but you still get frame protection and connect restrictions. It’s not perfect, but it’s better than nothing.

Handling Common WordPress Plugins

Google Analytics

Most people use a plugin for this. The policy needs:

s c i c o m r n g i n - p e s t c r - t c s - r s ' c r s c e ' l s ' f e s ' l e f l h ' f t ' t h p t h s t t : p t / s p / : s w / : w / / w w / . w w g w w o . w o g . g o g l o o e g o - l g a e l n t e a a - l g a y m n t a a i n l c a y s g t . e i c r c o . s m c . o c m o m

Contact Form 7

If you use Contact Form 7 with reCAPTCHA:

s f c r r a i m p e t - - s s r r c c ' ' s s e e l l f f ' ' h h t t t t p p s s : : w w w w w w . . g g o o o o g g l l e e . . c c o o m m r r e e c c a a p p t t c c h h a a h t t p s : / / w w w . g s t a t i c . c o m / r e c a p t c h a /

WooCommerce

WooCommerce adds Stripe, PayPal, and various other payment scripts:

s f c i f c r o m o r a n g r i m n - m p e e s - t - c r a - s t c c s r - t r c s ' i c r s o ' c e n ' s l s e ' f ' e l s ' s l f e e f ' l h l ' f t f h ' t ' h t p t t h s h t p t : t p s t / t s : p / p : s w s : w : j / w j s . / s . a p w . s p a w s t i y w t r . p . r i s a p i p t l a p e r o y e . i b p . c p j a c o e e l o m . c . m c t c h o s o h t m . m t t c t p o p s m s : : h w o w o w k . s p . a s y t p r a i l p . e c . o c m o m h t t p s : / / w w w . p a y p a l . c o m

Yoast SEO

Yoast itself doesn’t add external scripts, but if you enable their tracking:

c o n n e c t - s r c ' s e l f ' h t t p s : / / c o l l e c t . y o a s t . c o m

WP Rocket / Caching Plugins

These can interfere with CSP headers by caching pages with embedded nonces. If you use nonce-based CSP, exclude the CSP header from caching or use a cache-busting mechanism.

The Plugin Route

If editing PHP isn’t your thing, there are plugins that handle CSP:

  • HTTP Headers — Simple, lets you add any header including CSP
  • Security Headers — More comprehensive, includes other security headers too

The advantage of plugins is that they sometimes handle the admin/frontend split automatically. The disadvantage is less control over the exact policy.

Testing Checklist

  1. Start with report-only mode (change Content-Security-Policy to Content-Security-Policy-Report-Only)
  2. Browse your entire site — every page, every post, every category
  3. Check the browser console for violations
  4. Fill out and submit every form
  5. Test any interactive features (search, filters, infinite scroll)
  6. Check on mobile — some plugins load different scripts on mobile
  7. If everything looks good after a week, switch to enforcement

Verify Your Setup