2026年11月12日
2026年11月12日
WordPress Content Security Policy (CSP): Report-Only to Nonce
This article is a localized edition of the Japanese original. Read the Japanese original
When you introduce Content Security Policy (CSP) to WordPress, the safe approach is not to block things right away. Observe violations with Content-Security-Policy-Report-Only, refine your allowlist, and only then switch to enforcement. To make the policy effective against XSS, aim for a setup that permits scripts with a nonce instead of relying on 'unsafe-inline'. This article explains the step-by-step procedure and how to verify it, taking into account the pitfalls specific to WordPress (inline scripts, the admin screen and page caching).
What CSP can prevent, and what makes it hard on WordPress
CSP is a mechanism in which a page declares, in an HTTP header, which scripts, styles, images, connection targets and so on it may load, and the browser rejects everything else. For example, even if a <script> tag is injected through a vulnerability in the comment form or a plugin, that script does not run unless it is allowed.
On WordPress, however, a strict policy is hard to apply as it is, for the following reasons.
- Many inline scripts: There are configuration values output by functions such as
wp_localize_script(), and<script>tags that plugins write out directly. - Many inline styles: Global styles and decorations for blocks go into the HTML as
<style>tags orstyleattributes. - External services: The domains you need to allow differ from site to site, for example analytics, YouTube embeds and web fonts.
- The admin screen: If you apply the same policy as the front end to the Block editor, the editing screen may stop working.
For this reason, the approach is to "observe first," "leave the admin screen out," and "keep styles loose and tighten scripts first."
Main directives
The directives you will use most often are as follows.
default-src: The default that applies to resource types with no individual settingscript-src: Where JavaScript may be loaded from. The core of XSS protectionstyle-src: Where CSS may be loaded fromimg-src/font-src/media-src: Where images, fonts and audio/video may be loaded fromconnect-src: Connection targets forfetch(), XHR and WebSocket. The destinations that analytics sends data to are also covered hereframe-src: Where iframes embedded in the page may be loaded fromframe-ancestors: Whether other sites may embed your site in an iframe (clickjacking protection)object-src: Plugin elements such as Flash. Setting it to'none'is the usual choicebase-uri/form-action: Where the<base>tag may point, and where forms may submit
Step 1: Observe the current state with Report-Only
Start with the Content-Security-Policy-Report-Only header. This header only reports violations and blocks nothing. It lets you find out which resources run into the policy without breaking how the site looks or works.
This is an example of adding it to the front end only with Nginx. It assumes a site that uses Google Analytics (through Google Tag Manager), Google Fonts and YouTube embeds. Copy the configuration file before you change it, check the syntax with sudo nginx -t, and then reload.
# Inside the server block (write it on one line)
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data: https:; connect-src 'self' https://*.google-analytics.com https://*.analytics.google.com https://www.googletagmanager.com; frame-src https://www.youtube.com https://www.youtube-nocookie.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'" always;
If you put it in the server block, the same header is also added to /wp-admin/. That has no effect on behavior at the Report-Only stage, but at the enforcement stage you need to exclude the admin screen. So either switch to the PHP method described later, or write it only in the location for the front end. Also note that if you write add_header inside a location, the add_header directives in the server block are no longer sent for that location.
With Apache, write it as follows in .htaccess or the virtual host configuration (mod_headers is required).
<IfModule mod_headers.c>
Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"
</IfModule>
Once the header is in place, open the console in your developer tools and walk through the main pages. When there is a violation, a warning shows which URL violated which directive. Open every feature the site uses, such as article pages, the contact form, search results and, for WooCommerce, the cart and checkout screens. If you want to keep collecting violations, you can specify where reports are sent with report-uri (or report-to), but you need to set up something to receive them.
Step 2: Refine the allowlist and enforce
Add the legitimate sources that the observation found to the policy, and once the warnings stop, change the header name to Content-Security-Policy to enforce it. Do not apply it to the whole site at once. Switch during a low-traffic period, and check actual operations such as submitting the contact form and playing an embedded video.
One thing to be honest about here: a policy that includes 'unsafe-inline' in script-src has only limited effect as XSS protection. Inline scripts injected by an attacker can run too, so what it mainly protects against is "loading scripts from external domains you have not allowed." Restricting external domains, object-src 'none' and frame-ancestors are still meaningful, but to get the real benefit, move on to the nonce method below.
Step 3: Allow inline scripts with a nonce
A nonce is an unguessable value generated for each request. If you write 'nonce-random-value' in the script-src of the header and add the same nonce attribute only to legitimate <script> tags, scripts without that value do not run.
The wp_script_attributes and wp_inline_script_attributes filters, added in WordPress 5.7, let you add attributes to <script> tags that pass through WordPress's script output functions. In relatively recent versions, scripts registered with wp_enqueue_script() and inline scripts are also output through these functions (as of October 2026).
<?php
// Generate one nonce per request and reuse it
function wpdr_csp_nonce() {
static $nonce = null;
if ( null === $nonce ) {
$nonce = base64_encode( random_bytes( 16 ) );
}
return $nonce;
}
function wpdr_add_nonce_attr( $attributes ) {
$attributes['nonce'] = wpdr_csp_nonce();
return $attributes;
}
add_filter( 'wp_script_attributes', 'wpdr_add_nonce_attr' );
add_filter( 'wp_inline_script_attributes', 'wpdr_add_nonce_attr' );
// Send the header only on front-end pages (send_headers does not run on the admin screens or wp-login.php)
add_action( 'send_headers', function () {
$nonce = wpdr_csp_nonce();
$policy = "script-src 'nonce-{$nonce}' 'strict-dynamic' 'unsafe-inline' https:; "
. "object-src 'none'; base-uri 'self'; frame-ancestors 'self'";
header( 'Content-Security-Policy-Report-Only: ' . $policy );
} );
The 'unsafe-inline' and https: in this policy are a fallback for older browsers that do not support nonces. Browsers that understand nonces and 'strict-dynamic' ignore them. 'strict-dynamic' also allows other scripts that a nonce-approved script loads dynamically, which makes it a good fit for things like tag managers that load other scripts later.
Run this in Report-Only mode first as well, and check the violations that appear in the console. The following are likely to remain as violations.
- A
<script>that a theme or plugin writes directly withecho: It does not pass through a WordPress function, so no nonce is added. Change it to usewp_enqueue_script()orwp_print_inline_script_tag(), or ask the author to fix it. - Inline event attributes such as
onclick: A nonce cannot allow them, so rewrite them to register the events on the JavaScript side. - Custom HTML tags in a tag manager: You need to make the tag manager configuration pass the nonce along.
Page caching and nonces
Page caching is the biggest thing to watch out for with the nonce method. A nonce is meant to change with every request, but page caching reuses the HTML, so the nonce attributes in the cached HTML no longer match the nonce in the header sent at that time. If you enforce the policy, every script stops and the site does not work.
There are approaches such as caching the HTML and the headers together in the cache plugin or CDN (the same nonce is reused, so it is not as strong as a value that changes every time) or using a mechanism that swaps the nonce at the edge, but the difficulty varies greatly with your setup. For a site that uses page caching, the realistic approach is to first narrow down the external domains with the allowlist method, and decide on the nonce method only after testing it thoroughly in a staging environment.
How to verify the settings
- Run
curl -I https://example.com/and check that exactly one of the intended header is returned. If both the server configuration and a plugin send CSP, you get two headers, and only what satisfies both is allowed. - Check on the main pages that the developer tools console shows no violation warnings.
- If you paste the policy into a tool such as Google's CSP Evaluator, it points out weaknesses such as the effect of
'unsafe-inline'.
You can also specify CSP with <meta http-equiv="Content-Security-Policy">, but a meta tag cannot use Report-Only or frame-ancestors. On WordPress, use the HTTP header method as your basic approach.
Summary
- Observe with
Content-Security-Policy-Report-Onlyfirst, then switch CSP to enforcement - WordPress has many inline scripts and styles, and it is safest to leave the admin screen out of the policy
- An allowlist that includes
'unsafe-inline'mainly restricts external domains and is of limited use as XSS protection - Adding a nonce with the
wp_script_attributes/wp_inline_script_attributesfilters and combining it with'strict-dynamic'makes the policy more effective - The nonce method conflicts with page caching, so introduce it after checking your setup
- Use
curl -Ito check that headers are not duplicated, and use the console to check that no violations remain
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)