2026年11月12日
2026年11月12日
WordPress Content Security Policy (CSP): From Report-Only to Nonces
This article is a localized edition of the Japanese original. Read the Japanese original
Content Security Policy (CSP) is an HTTP header that tells the browser where a page may load scripts, styles, images and other resources from. Set up correctly, it makes scripts injected through XSS (cross-site scripting) much harder to run. Set up wrongly, it breaks features of your site. This article shows how to roll it out on WordPress in the following order.
- Use
Content-Security-Policy-Report-Onlyto see what would be a violation - Collect violation reports
- Apply an allowlist policy
- If needed, move to a stricter nonce-based policy
The code assumes WordPress 6.x as of October 2026.
The two approaches
| Approach | Example | Pros | Cons |
|---|---|---|---|
| Allowlist | script-src 'self' 'unsafe-inline' https://www.googletagmanager.com |
Existing themes and plugins rarely break | With 'unsafe-inline', injected inline scripts also run, so the protection against XSS is limited |
| Nonce | script-src 'nonce-random-value' 'strict-dynamic' |
Only scripts carrying the correct nonce run, which gives strong protection against XSS | Inline scripts and event attributes that cannot carry a nonce stop working. Does not mix well with page caching |
The practical path is to start with an allowlist and, once the violations are gone, consider a nonce policy if you need it.
Step 1: Find violations with Report-Only
The Content-Security-Policy-Report-Only header does not block resources that violate the policy; it only reports them. You can see what would be caught without changing how the site behaves.
Put the code in your theme's functions.php, or in a PHP file in wp-content/mu-plugins/ so that it survives a theme change.
<?php
// Send CSP as Report-Only (report violations, block nothing)
add_action( 'send_headers', function () {
$directives = array(
"default-src 'self'",
"script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"img-src 'self' data: https:",
"font-src 'self' data: https://fonts.gstatic.com",
"connect-src 'self' https://www.google-analytics.com",
"frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'self'",
'report-uri ' . esc_url_raw( rest_url( 'csp/v1/report' ) ),
);
header( 'Content-Security-Policy-Report-Only: ' . implode( '; ', $directives ) );
} );
The external domains are examples. Add or remove them to match the services you actually use. report-uri points, via rest_url(), to the endpoint you create in the next step. This produces the correct URL even when the permalink setting is "Plain".
Violations also appear in the "Console" of the browser's developer tools. Open the main pages one by one, such as the home page, posts, pages, the contact form and pages with embedded videos, and check them.
Main directives
| Directive | What it controls |
|---|---|
default-src |
The default for resource types you do not specify separately |
script-src |
JavaScript |
style-src |
CSS |
img-src |
Images. data: allows inline images and https: allows any image over HTTPS |
font-src |
Web fonts |
connect-src |
Destinations of fetch() and Ajax requests |
frame-src |
What can be embedded in an iframe |
object-src |
<object> and <embed>. 'none' forbids them |
base-uri |
URLs allowed in the <base> tag |
form-action |
Where forms can be submitted |
frame-ancestors |
Sites that may embed your pages in an iframe (clickjacking protection) |
If the reports mention eval, a theme or plugin uses eval() or similar. If you need that script, add 'unsafe-eval' to script-src, but security is reduced.
Step 2: Create an endpoint for violation reports
When a violation occurs, the browser POSTs JSON (with the Content-Type application/csp-report) to the URL in report-uri. This example receives it through the WordPress REST API and writes it to the PHP error log.
<?php
// Receive CSP violation reports and write them to the error log
add_action( 'rest_api_init', function () {
register_rest_route(
'csp/v1',
'/report',
array(
'methods' => 'POST',
'permission_callback' => '__return_true', // Browsers send reports without authentication
'callback' => function ( WP_REST_Request $request ) {
$body = $request->get_body();
// Do not log oversized submissions
if ( strlen( $body ) > 0 && strlen( $body ) <= 10000 ) {
$report = json_decode( $body, true );
if ( is_array( $report ) && isset( $report['csp-report'] ) ) {
$r = $report['csp-report'];
error_log(
sprintf(
'CSP violation: %s blocked=%s page=%s',
$r['violated-directive'] ?? '',
$r['blocked-uri'] ?? '',
$r['document-uri'] ?? ''
)
);
}
}
return new WP_REST_Response( null, 204 );
},
)
);
} );
The log goes to wp-content/debug.log if WP_DEBUG_LOG is enabled in wp-config.php, and to the server's PHP error log otherwise. Anyone can post to this endpoint, so on a busy site the log grows quickly. When you are done checking, remove report-uri from the policy or delete the endpoint.
Note that newer specifications are replacing report-uri with report-to, but as of October 2026 report-uri still works in the major browsers.
Step 3: Apply an allowlist policy
Using the reports, add the domains you need and remove the ones you do not. Once the main pages no longer produce violations, change the header name to Content-Security-Policy to enforce it.
<?php
// Change only the header name in the code from Step 1
header( 'Content-Security-Policy: ' . implode( '; ', $directives ) );
Keep report-uri for a while after enforcing to monitor violations, and also test features administrators rarely use (registration, payments, search and so on).
Step 4: Move to a nonce policy (advanced)
With a nonce policy, you generate a random value (nonce) for each request and put the same value in both the CSP header and every <script> tag you want to allow. Scripts whose value does not match do not run.
In WordPress, the tags of scripts loaded with wp_enqueue_script() are printed by wp_get_script_tag(), and inline scripts such as those from wp_add_inline_script(), wp_localize_script() and translation data are printed by wp_get_inline_script_tag(). You can add attributes with the wp_script_attributes and wp_inline_script_attributes filters respectively, so that is where you add the nonce. Replacing strings in the script_loader_tag filter misses some inline scripts, such as the data from wp_localize_script(), so use these two filters instead.
<?php
// Nonce-based CSP (try it in Report-Only first)
// Create one nonce per request
function my_csp_nonce() {
static $nonce = null;
if ( null === $nonce ) {
$nonce = base64_encode( random_bytes( 16 ) );
}
return $nonce;
}
// Add the nonce to tags of scripts loaded with wp_enqueue_script()
add_filter( 'wp_script_attributes', function ( $attributes ) {
$attributes['nonce'] = my_csp_nonce();
return $attributes;
} );
// Add the nonce to inline scripts such as wp_add_inline_script()
add_filter( 'wp_inline_script_attributes', function ( $attributes ) {
$attributes['nonce'] = my_csp_nonce();
return $attributes;
} );
add_action( 'send_headers', function () {
$directives = array(
"default-src 'self'",
"script-src 'nonce-" . my_csp_nonce() . "' 'strict-dynamic'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"object-src 'none'",
"base-uri 'self'",
'report-uri ' . esc_url_raw( rest_url( 'csp/v1/report' ) ),
);
header( 'Content-Security-Policy-Report-Only: ' . implode( '; ', $directives ) );
} );
'strict-dynamic' also allows scripts that a nonce-carrying script loads later. You may need it for services such as Google Tag Manager that load other scripts. In browsers that support 'strict-dynamic', any domain allowlist in script-src is ignored.
What stops working with a nonce policy
Once a nonce policy is enforced, the following scripts do not run. Check the reports during the Report-Only phase and deal with them one at a time.
- Scripts that theme templates or plugins print directly as
<script>: rewrite them to usewp_enqueue_script(),wp_add_inline_script()orwp_print_inline_script_tag(). Analytics tags pasted directly into header.php are included - Event attributes such as
onclick="…"and links that start withjavascript:: a nonce cannot allow them. Rewrite them as scripts that useaddEventListener() - Anything that still relies on
'unsafe-inline'after you add a nonce: whenscript-srccontains a nonce, browsers ignore'unsafe-inline'. Writing both does not allow inline scripts
How page caching interferes
The nonce must change on every request. If a page caching plugin or a CDN stores the HTML and serves it again, the nonce embedded in the HTML no longer matches the nonce in the header, and scripts stop running. And if caching hands the same nonce to many visitors, the nonce loses its purpose. On a site that uses page caching, either stay with an allowlist policy or confirm that your caching setup supports nonces before you adopt this approach.
Step 5: Check the setup
Use curl to check that the header is sent. Replace example.com with your domain.
curl -sI https://example.com/ | grep -i "content-security-policy"
You can also select the page in the "Network" tab of the browser's developer tools and look at the response headers. To find weaknesses in a policy, paste it into Google's CSP Evaluator.
Caveats
send_headersruns when WordPress renders a front-end page. The admin and the login screen do not get the headers from this article- If .htaccess or your Nginx configuration also sends a CSP, you end up with two headers. The browser only allows resources that satisfy both policies, which causes unexpected blocking. Set CSP in one place only
- After adding or updating themes and plugins, check the Report-Only reports and the console for new violations
Summary
- Start by sending
Content-Security-Policy-Report-Onlyand use the console and the violation reports to find what gets caught - Create a report endpoint with
register_rest_route()and pointreport-urito it withrest_url() - An allowlist policy rarely breaks things, but as long as it contains
'unsafe-inline'its protection against XSS is limited - For a nonce policy, add the nonce with the
wp_script_attributesandwp_inline_script_attributesfilters. Directly printed<script>tags, event attributes andjavascript:links stop working, and'unsafe-inline'is ignored - On sites with page caching, check compatibility with the cache before adopting a nonce policy
Related articles:
- How to test a WordPress Content Security Policy (CSP) safely in report mode
- How to fix Content Security Policy (CSP) errors in WordPress
- How to set security headers in WordPress: .htaccess, Nginx and functions.php
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)