How to Write Secure CSP for WordPress Sites
Practical guide to deploying Content Security Policy on WordPress. Learn how to handle inline scripts from plugins and themes, add nonces, configure WooCommerce compatibility, and set different policies for admin vs frontend.
WordPress powers over 40% of the web. It is also one of the hardest platforms to deploy a Content Security Policy on. The reason is not WordPress itself. It is everything that runs on top of it. Every plugin you install can inject inline scripts into your pages. Every theme adds its own inline styles. The block editor generates inline CSS for every layout choice. And all of this happens without any awareness that a CSP might exist.
The result is that most WordPress site owners who try to add a CSP give up within an hour. They add a strict policy, their entire site breaks, and they conclude that CSP does not work with WordPress. But it does work. It just requires understanding where the inline code comes from and how to deal with it systematically.
This guide walks through the specific challenges of CSP on WordPress and the practical solutions that actually work in production. No vague advice. Real examples for real WordPress setups, including WooCommerce, page builders, and popular plugin stacks.
Why WordPress and CSP collide#
A strict Content Security Policy says "block all inline scripts and styles unless they are explicitly trusted." WordPress, by design, puts inline scripts and styles everywhere. This is the core conflict.
WordPress core itself adds inline scripts for things like the emoji detection script, the comment reply script, and various admin bar functions. Then your theme adds inline CSS for custom colors, fonts, and layout settings configured in the customizer. Then every plugin you install can call wp_add_inline_script() and wp_add_inline_style() to inject whatever they need directly into your HTML.
A typical WordPress page with a handful of plugins might have 10 to 20 inline script blocks and a similar number of inline style blocks. Each one of these will trigger a CSP violation if your policy does not account for them.
The unsafe inline trap
The most common "solution" you will find on WordPress forums is to add 'unsafe-inline' 'unsafe-eval' to your CSP. This makes everything work, but it also makes your CSP useless. If your policy allows unsafe-inline, an attacker who finds an XSS vulnerability can still inject and execute arbitrary scripts. You have the header, but it provides no protection.
Where the inline scripts come from#
Before you can fix the problem, you need to understand what is generating inline code on your pages. Here are the most common sources on a typical WordPress site:
- WordPress core. The emoji detection script (
wp-emoji-release.min.js), the comment reply handler, admin bar JavaScript, and variouswp_localize_script()calls that pass PHP data to JavaScript as inline JSON objects. - Your theme. Custom CSS from the customizer is output as an inline
<style>block. Many themes also add inline scripts for mobile menu toggles, scroll effects, and lazy loading. - Gutenberg (block editor). The block editor generates inline CSS for custom block styles, colors, spacing, and typography settings. Every block with custom styling adds its own inline style output.
- Plugins. Contact forms, analytics trackers, caching plugins, SEO plugins, sliders, popups, and WooCommerce all inject their own inline scripts. Some use
wp_add_inline_script()properly, others print raw<script>tags directly. - Page builders. Elementor, Divi, WPBakery, and similar builders generate significant amounts of inline CSS and JavaScript for their custom layouts and animations.
Strategy 1: Nonces (the recommended approach)#
The strongest approach for WordPress is nonce based CSP. You generate a random token on every page load, attach it to every inline script and style tag, and include the same token in your CSP header. The browser will only execute inline code that carries the correct nonce.
Since WordPress 6.1, there is built in support for adding attributes to script tags via the wp_script_attributes filter. This is the cleanest way to add nonces to scripts that WordPress enqueues properly.
// Generate a nonce once per request
function csp_get_nonce() {
static $nonce = null;
if ($nonce === null) {
$nonce = wp_create_nonce('csp-nonce-' . time());
// Or use: $nonce = bin2hex(random_bytes(16));
}
return $nonce;
}
// Add nonce to enqueued script tags (WP 6.1+)
add_filter('wp_script_attributes', function($attributes) {
$attributes['nonce'] = csp_get_nonce();
return $attributes;
});
// Add nonce to inline script tags
add_filter('wp_inline_script_attributes', function($attributes) {
$attributes['nonce'] = csp_get_nonce();
return $attributes;
});
// Add nonce to style tags
add_filter('wp_style_attributes', function($attributes) {
$attributes['nonce'] = csp_get_nonce();
return $attributes;
});Then set the CSP header with the same nonce value. You can do this in your theme's functions.php or in a custom plugin:
// Send CSP header (only on frontend, not admin)
add_action('send_headers', function() {
if (is_admin()) {
return; // Skip admin area
}
$nonce = csp_get_nonce();
header(sprintf(
"Content-Security-Policy: " .
"default-src 'self'; " .
"script-src 'self' 'nonce-%s' 'strict-dynamic'; " .
"style-src 'self' 'nonce-%s'; " .
"img-src 'self' data: https:; " .
"font-src 'self' data:; " .
"connect-src 'self'; " .
"frame-src 'self'; " .
"report-uri /wp-json/csp/v1/report",
$nonce, $nonce
));
});Why strict-dynamic matters here
The 'strict-dynamic' keyword in script-src tells the browser that any script loaded by an already trusted (nonced) script is also trusted. This is critical for WordPress because many plugins load additional scripts dynamically. Without strict-dynamic, you would need to nonce every single dynamically loaded script, which is often impossible with third party plugins.
The plugin problem: scripts that bypass the hook system#
The nonce approach works perfectly for scripts and styles that go through the WordPress enqueue system. But not every plugin plays by the rules. Some plugins print raw <script> tags directly using wp_head, wp_footer, or even directly in template files. These scripts will not have your nonce and will be blocked by CSP.
The only way to find these is to test. Deploy your CSP in Content-Security-Policy-Report-Only mode and check the violation reports. When you find a plugin that prints raw script tags, you have a few options:
- Use output buffering. Capture the final HTML output with
ob_start()and inject nonces into all script and style tags before sending the response. This is a brute force approach, but it catches everything. - Replace the plugin. If a plugin does not use the WordPress script enqueue system, it is likely poorly maintained. Consider switching to an alternative that follows WordPress coding standards.
- Use hash based CSP for static scripts. If the inline script content never changes (like a static analytics snippet), compute its SHA-256 hash and add it to your policy instead of using a nonce.
// Catch all output and inject nonces
add_action('template_redirect', function() {
if (is_admin()) return;
ob_start(function($html) {
$nonce = csp_get_nonce();
// Add nonce to script tags that don't have one
$html = preg_replace(
'/<script(?![^>]*nonce)([^>]*)>/i',
'<script nonce="' . $nonce . '"$1>',
$html
);
// Add nonce to style tags that don't have one
$html = preg_replace(
'/<style(?![^>]*nonce)([^>]*)>/i',
'<style nonce="' . $nonce . '"$1>',
$html
);
return $html;
});
});Output buffering and caching
If you use a page caching plugin (WP Super Cache, W3 Total Cache, WP Rocket), the output buffering approach needs special handling. The nonce must be different on every request, but cached pages serve the same HTML to everyone. You will need to either exclude pages from cache or use a caching plugin that supports dynamic nonce injection. Some managed hosts like Cloudflare and Kinsta offer edge based header injection that can help with this.
Admin vs. frontend: use different policies#
This is a mistake almost everyone makes on their first attempt. They try to create one CSP that works for both the WordPress admin dashboard and the public frontend. Do not do this. The admin area and the frontend have completely different requirements.
The WordPress admin uses Gutenberg's block editor (which relies heavily on eval() and inline styles), the media library, the theme customizer, and dozens of other features that all need permissive CSP rules. Trying to lock down the admin with a strict CSP will break the editor, prevent media uploads, and make the dashboard unusable.
add_action('send_headers', function() {
// Only apply strict CSP to the public frontend
if (is_admin() || is_login_page()) {
return;
}
$nonce = csp_get_nonce();
header(sprintf(
"Content-Security-Policy: " .
"default-src 'self'; " .
"script-src 'self' 'nonce-%s' 'strict-dynamic'; " .
"style-src 'self' 'nonce-%s'; " .
"img-src 'self' data: https:; " .
"font-src 'self' data: https://fonts.gstatic.com; " .
"connect-src 'self'; " .
"report-uri /wp-json/csp/v1/report",
$nonce, $nonce
));
});
// Helper function
function is_login_page() {
return in_array($GLOBALS['pagenow'],
['wp-login.php', 'wp-register.php']);
}If you want some protection on the admin area, you can set a separate, more permissive policy there. But the priority should be securing the frontend first, since that is what your visitors interact with and where XSS attacks would target.
WooCommerce and payment gateways#
WooCommerce adds its own layer of complexity. The cart and checkout pages use inline scripts for AJAX cart updates, form validation, and payment processing. Payment gateways load external scripts from their own domains.
Here are the most common domains you will need to allowlist for popular payment gateways:
| Gateway | Domains to allowlist | Directives |
|---|---|---|
| Stripe | js.stripe.com, api.stripe.com | script-src, frame-src, connect-src |
| PayPal | www.paypal.com, www.paypalobjects.com | script-src, frame-src, connect-src |
| Square | js.squareup.com, connect.squareup.com | script-src, frame-src, connect-src |
| Braintree | js.braintreegateway.com, assets.braintreegateway.com | script-src, frame-src, connect-src |
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{value}' 'strict-dynamic'
js.stripe.com;
style-src 'self' 'nonce-{value}';
frame-src 'self' js.stripe.com;
connect-src 'self' api.stripe.com;
img-src 'self' data: https:;
font-src 'self' data:;
report-uri /wp-json/csp/v1/reportCommon third party services and their CSP requirements#
Beyond payment gateways, most WordPress sites load resources from several other external services. Here are the domains you will commonly need to add to your policy:
- Google Fonts:
fonts.googleapis.cominstyle-srcandfonts.gstatic.cominfont-src - Google Analytics / Tag Manager:
www.googletagmanager.comandwww.google-analytics.cominscript-srcandconnect-src - YouTube embeds:
www.youtube.comandwww.youtube-nocookie.cominframe-src - Gravatar:
secure.gravatar.comandi0.wp.cominimg-src - reCAPTCHA:
www.google.comandwww.gstatic.cominscript-srcandframe-src
WordPress plugins that help with CSP#
If you prefer not to write custom code, there are plugins that can help manage your CSP. None of them are perfect, but they can save time on initial setup:
- Really Simple Security (formerly Really Simple SSL) includes security header management with a CSP builder in its Pro version. It provides a UI for building your policy and can auto detect some required sources.
- HTTP Headers lets you set all security headers including CSP from the WordPress dashboard. It provides a directive by directive interface but does not handle nonce injection automatically.
- Headers Security Advanced & HSTS WP is a lightweight option for setting security headers. Good for basic CSP configuration without the overhead of a larger security plugin.
Regardless of which approach you use, no plugin can automatically solve all CSP conflicts. You still need to test your policy, review violation reports, and update it when you add new plugins or change themes. Use the CSP Generator to build your initial policy, then refine it based on real world testing.
Setting CSP at the server level#
If you prefer to set the CSP in your web server config rather than in PHP, you can do that. The downside is that you lose the ability to use dynamic nonces (since the server config is static). But for sites that can use hash based policies, this works well.
# Only apply to non-admin pages
location / {
# Skip CSP for admin and login pages
if ($request_uri ~* "^/wp-admin|^/wp-login") {
break;
}
add_header Content-Security-Policy
"default-src 'self'; script-src 'self' https://js.stripe.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; frame-src 'self' https://js.stripe.com; connect-src 'self' https://api.stripe.com; report-uri /wp-json/csp/v1/report;"
always;
}Note that the Nginx example above uses 'unsafe-inline' for style-src because without dynamic nonces, there is no clean way to handle all the inline styles that WordPress and its themes generate. This is a common tradeoff for server level WordPress CSP: strict on scripts (the most important protection), more permissive on styles.
Step by step: rolling out CSP on your WordPress site#
Audit your current page
Open your site in a browser, inspect the page source, and list every external domain your site loads resources from. Check scripts, styles, fonts, images, and iframes. Use the SiteSecurityScore scanner to get a detailed breakdown.
Build your initial policy
Use the CSP Generator to create a policy that includes all the domains you identified. Start with a policy that covers your known resources.
Deploy in report only mode
Use Content-Security-Policy-Report-Only with a report-uri endpoint. Leave it running for at least one to two weeks to catch violations across all pages, user flows, and plugin interactions.
Fix violations and refine
Review your violation reports (filter out browser extension noise). Add missing domains to your policy. Handle inline scripts with nonces or output buffering. Test every major page: homepage, blog posts, contact forms, checkout if using WooCommerce.
Switch to enforcing mode
Once your reports are consistently clean, switch from Content-Security-Policy-Report-Only to Content-Security-Policy. Keep the report-uri active so you catch new violations from plugin updates or content changes.
Common mistakes with WordPress CSP#
Forgetting to test after plugin updates
Plugin updates can introduce new inline scripts, new external domains, or change how existing scripts are loaded. After any plugin or theme update, check your CSP violation reports to see if anything new is being blocked.
Applying the same policy to admin and frontend
The WordPress admin dashboard needs a much more permissive policy than the public site. Either skip CSP on admin pages or create a separate, looser policy for them.
Using unsafe-inline as a permanent solution
It is fine to start with unsafe-inline in style-src as a temporary measure while you work on nonces. But never leave unsafe-inline in script-src. That is where the real protection is.
Not accounting for caching
If you use nonce based CSP with a page caching plugin, cached pages will serve stale nonces that do not match the CSP header. Either exclude dynamic pages from cache, use a caching setup that supports dynamic nonce injection, or use hash based CSP for cached content.
The bottom line#
CSP on WordPress is harder than on most platforms because of the sheer number of plugins and themes injecting inline code. But it is absolutely doable. The key is to approach it methodically: separate admin from frontend, use nonces for inline scripts, start in report only mode, and refine based on real violation data.
You do not need a perfect CSP on day one. Even a basic policy that blocks unsafe-inline for scripts and uses nonces gives you meaningful XSS protection. You can tighten the style and image directives over time. The important thing is to start.
Run a scan with SiteSecurityScore to see where your WordPress site stands right now, then use the steps above to build and deploy a policy that actually protects your visitors.
Frequently asked questions#
Why does adding a Content Security Policy break my WordPress site?
WordPress core, themes, and plugins all inject inline scripts and styles. A strict CSP blocks inline code by default. The fix is to use nonces (random tokens added to each inline script and style) or to identify and allowlist specific sources your site needs.
How do I add CSP nonces to WordPress inline scripts?
WordPress 6.1+ supports the wp_script_attributes and wp_inline_script_attributes filters. Generate a nonce once per request, add it through these filters, and include the same value in your CSP header. For plugins that bypass the enqueue system, use output buffering to inject nonces into all script tags.
Should I use the same CSP for the WordPress admin and the frontend?
No. The admin dashboard uses Gutenberg, the media library, and many features that require a permissive policy. Apply your strict CSP only to the frontend. Use is_admin() in your header sending function to skip or apply a different policy on admin pages.
Does WooCommerce work with Content Security Policy?
Yes, but it needs careful configuration. WooCommerce uses inline scripts for cart and checkout functionality, and payment gateways load scripts from external domains (like js.stripe.com or www.paypal.com). Allowlist these domains in your script-src, frame-src, and connect-src directives.
Are there WordPress plugins that help manage Content Security Policy?
Yes. Plugins like Really Simple Security, HTTP Headers, and Headers Security Advanced & HSTS WP can help set CSP headers from the dashboard. However, no plugin automatically solves all conflicts. You still need to test thoroughly, review violation reports, and update your policy when you install new plugins or change themes.
Continue reading
How secure is your WordPress site?
Scan your WordPress site to check your CSP, security headers, and overall security posture.