Content Security Policy (CSP) is a crucial security layer that helps protect your WordPress site from cross-site scripting (XSS), data injection, and other attacks. However, it can often block legitimate inline scripts and styles, leading to functionality issues or broken layouts.
Here’s how to fix CSP inline script and style issues in WordPress, moving from less secure to more secure methods:
Understanding the Problem
When you enable CSP, it typically blocks:
<script>
tags with inline JavaScript.- Inline event handlers (e.g.,
onclick="myFunction()"
). javascript:
URLs.eval()
and similar unsafe APIs.<style>
tags with inline CSS.style
attributes (e.g.,<div style="color:red;">
).
The browser console will show CSP violation errors, indicating which directives are being violated (e.g., script-src
, style-src
).
Recommended Solutions (Most Secure)
1. Use Nonces (Recommended for Scripts and Styles)
Nonces (numbers used once) are cryptographic tokens that allow you to whitelist specific inline scripts and styles. A unique nonce is generated for each HTTP request, and you include it in your CSP header and in the nonce
attribute of the inline <script>
or <style>
tags.
How it works:
- Generate a nonce: WordPress provides
wp_create_nonce()
. - Add nonce to CSP header: You’ll need to dynamically add the nonce to your
Content-Security-Policy
header. This usually involves PHP code in yourfunctions.php
or a dedicated plugin. - Add nonce attribute to inline tags: For any inline
<script>
or<style>
tags, you addnonce="YOUR_GENERATED_NONCE"
.
Example (Conceptual):
In functions.php
(or a custom plugin):
<?php // Generate a new nonce for each request function my_generate_csp_nonce() { $nonce = wp_create_nonce( 'my-csp-nonce-action' ); // Use a unique action string // Store the nonce globally or pass it to a filter for CSP header modification // For simplicity, let's store it in a global variable (less ideal but demonstrates) global $my_csp_nonce; $my_csp_nonce = $nonce; // Set the CSP header with the nonce // This is simplified; a real implementation might use a plugin or more robust header management header( "Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{$nonce}'; style-src 'self' 'nonce-{$nonce}';" ); } add_action( 'send_headers', 'my_generate_csp_nonce' ); // Example of adding inline script with nonce function my_enqueue_script_with_nonce() { global $my_csp_nonce; if ( ! empty( $my_csp_nonce ) ) { wp_enqueue_script( 'my-custom-script', get_template_directory_uri() . '/js/my-custom-script.js', array(), '1.0', true ); wp_add_inline_script( 'my-custom-script', 'alert("Hello from inline script!");', 'after', array( 'nonce' => $my_csp_nonce ) ); // Pass nonce via array } } add_action( 'wp_enqueue_scripts', 'my_enqueue_script_with_nonce' ); // Example of adding inline style with nonce function my_enqueue_style_with_nonce() { global $my_csp_nonce; if ( ! empty( $my_csp_nonce ) ) { wp_enqueue_style( 'my-custom-style', get_template_directory_uri() . '/css/my-custom-style.css', array(), '1.0' ); $custom_css = '.my-element { color: blue; }'; wp_add_inline_style( 'my-custom-style', $custom_css, array( 'nonce' => $my_csp_nonce ) ); // No direct nonce attribute for wp_add_inline_style // Note: wp_add_inline_style handles nonces if the enqueued style does. // For inline <style> tags generated directly in templates, you'd need to manually add the nonce. } } add_action( 'wp_enqueue_scripts', 'my_enqueue_style_with_nonce' ); ?>
Important Note for wp_add_inline_script()
and wp_add_inline_style()
: WordPress’s wp_add_inline_script()
and wp_add_inline_style()
functions are designed to work with enqueued scripts/styles. If your CSP implementation is correctly integrated with WordPress’s enqueue system (e.g., via a plugin that hooks into wp_print_scripts
or wp_print_styles
), these functions might automatically handle nonce insertion. However, if you are manually setting the CSP header, you might need to ensure the nonce is applied.
WordPress Nonce-based CSP Implementation Plugin:
<?php /** * Plugin Name: Custom CSP Nonce * Description: Implements nonce-based CSP for WordPress. * Version: 1.0 * Author: Your Name */ // 1. Generate a unique nonce for each request function custom_csp_generate_nonce() { // Generate a cryptographically secure random string // This nonce will be unique for every page load $nonce = base64_encode( wp_generate_password( 32, true, true ) ); define( 'CSP_NONCE', $nonce ); } add_action( 'init', 'custom_csp_generate_nonce' ); // 2. Add the CSP header with the generated nonce function custom_csp_add_header() { if ( ! defined( 'CSP_NONCE' ) ) { return; // Nonce not defined, something went wrong } $nonce = CSP_NONCE; // Define your CSP policy. Adjust directives as needed for your site. // 'self' allows resources from the same origin. // 'nonce-' allows inline scripts/styles with the matching nonce. // 'strict-dynamic' allows scripts loaded by a nonced script to execute, // which is useful for dynamically loaded scripts (e.g., by React, Vue). // If using 'strict-dynamic', you might not need 'self' for script-src. // For simplicity, we'll include 'self' and 'nonce'. $csp_policy = array( "default-src 'self'", "script-src 'self' 'nonce-{$nonce}'", // Allow self and nonced scripts "style-src 'self' 'nonce-{$nonce}'", // Allow self and nonced styles "img-src 'self' data:", // Allow images from self and data URIs "font-src 'self' data:", // Allow fonts from self and data URIs "connect-src 'self'", // Allow connections to self (AJAX, WebSockets) "frame-ancestors 'self'", // Prevent clickjacking "form-action 'self'", // Restrict form submissions to same origin "object-src 'none'", // Block plugins (Flash, Java) "base-uri 'self'", // Restrict base URL "report-uri /csp-report-endpoint", // Optional: Send violation reports to this URL ); // Filter to allow other plugins/themes to modify the CSP policy $csp_policy = apply_filters( 'custom_csp_policy', $csp_policy, $nonce ); // Join the policy directives with a semicolon and add the header header( "Content-Security-Policy: " . implode( '; ', $csp_policy ) ); } // Add the header early, before any output is sent add_action( 'send_headers', 'custom_csp_add_header' ); // 3. Modify WordPress inline script/style functions to include the nonce // This requires modifying how WordPress adds inline scripts/styles. // For scripts enqueued with wp_enqueue_script and then inline scripts added with wp_add_inline_script: function custom_csp_add_nonce_to_inline_script( $tag, $handle, $src ) { if ( ! defined( 'CSP_NONCE' ) ) { return $tag; } // Check if it's an inline script (no src attribute) if ( empty( $src ) && strpos( $tag, '<script' ) !== false ) { // Add nonce attribute if it's not already there if ( strpos( $tag, 'nonce="' ) === false ) { $tag = str_replace( '<script', '<script nonce="' . CSP_NONCE . '"', $tag ); } } return $tag; } // This filter is for scripts added via wp_add_inline_script add_filter( 'script_loader_tag', 'custom_csp_add_nonce_to_inline_script', 10, 3 ); // For inline styles added with wp_add_inline_style: function custom_csp_add_nonce_to_inline_style( $tag, $handle, $href ) { if ( ! defined( 'CSP_NONCE' ) ) { return $tag; } // Check if it's an inline style (no href attribute) if ( empty( $href ) && strpos( $tag, '<style' ) !== false ) { // Add nonce attribute if it's not already there if ( strpos( $tag, 'nonce="' ) === false ) { $tag = str_replace( '<style', '<style nonce="' . CSP_NONCE . '"', $tag ); } } return $tag; } // This filter is for styles added via wp_add_inline_style add_filter( 'style_loader_tag', 'custom_csp_add_nonce_to_inline_style', 10, 3 ); // IMPORTANT: For inline scripts/styles generated directly by themes or plugins // (i.e., not using wp_add_inline_script/style), you will need to manually // modify those theme/plugin files to include the nonce attribute. // Example: // <script nonce="<?php echo CSP_NONCE; ?>">...</script> // <style nonce="<?php echo CSP_NONCE; ?>">...</style> // Helper to get the nonce for manual inclusion in templates if ( ! function_exists( 'get_csp_nonce' ) ) { function get_csp_nonce() { return defined( 'CSP_NONCE' ) ? CSP_NONCE : ''; } } // Example of how to use get_csp_nonce in your theme template files: // <script nonce="<?php echo get_csp_nonce(); ?>"> // // Your inline JavaScript here // </script> // <style nonce="<?php echo get_csp_nonce(); ?>"> // /* Your inline CSS here */ // </style> ?>
2. Use Hashes (More Complex for Dynamic Content)
You can compute a SHA256, SHA384, or SHA512 hash of your inline script or style content and include that hash in your CSP header.
How it works:
- Calculate the hash of the exact content of your inline script or style.
- Add the hash to your CSP directive (e.g.,
script-src 'sha256-YOURHASHHERE'
).
Challenges:
- This is difficult for dynamic content, as the hash changes with every modification.
- Maintaining hashes for all inline scripts/styles across a large WordPress site (especially with plugins and themes) is very challenging.
When to use: For static, non-changing inline scripts or styles that you can control entirely.
3. Refactor Inline Code (Best Practice)
The most robust solution is to eliminate inline scripts and styles as much as possible.
For Scripts:
- Move to external
.js
files: Enqueue your scripts usingwp_enqueue_script()
. - Use
wp_localize_script()
: If you need to pass dynamic PHP data to your JavaScript, usewp_localize_script()
. This function localizes a registered script with data, creating a JavaScript object that your script can access. This eliminates the need for inline<script>
blocks for data.function my_theme_scripts() { wp_enqueue_script( 'my-script', get_template_directory_uri() . '/js/my-script.js', array(), '1.0', true ); wp_localize_script( 'my-script', 'myAjax', array( 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'my-ajax-nonce' ) // Always use nonces for AJAX )); } add_action( 'wp_enqueue_scripts', 'my_theme_scripts' );
Then in
my-script.js
:jQuery(document).ready(function($) { $.post(myAjax.ajaxurl, { action: 'my_action', nonce: myAjax.nonce }, function(response) { console.log(response); }); });
- Use
wp_add_inline_script()
: For very small, highly specific inline scripts tied to an enqueued script,wp_add_inline_script()
is a safe and recommended alternative to directly embeddingscript
tags. It allows you to append or prepend script code to an already registered script.function my_custom_inline_script() { wp_enqueue_script( 'my-main-script', get_template_directory_uri() . '/js/main.js', array(), '1.0', true ); wp_add_inline_script( 'my-main-script', 'console.log("This is an inline script added securely.");', 'after' ); } add_action( 'wp_enqueue_scripts', 'my_custom_inline_script' );
For Styles:
- Move to external
.css
files: Enqueue your stylesheets usingwp_enqueue_style()
. - Use wp_add_inline_style(): Similar to scripts, for small, dynamic CSS that needs to be generated by PHP, use wp_add_inline_style().
function my_custom_inline_style() { wp_enqueue_style( 'my-main-style', get_template_directory_uri() . '/css/main.css', array(), '1.0' ); $dynamic_color = '#' . get_theme_mod( 'primary_color', '000000' ); // Example: get color from theme options $custom_css = ".site-title { color: {$dynamic_color}; }"; wp_add_inline_style( 'my-main-style', $custom_css ); } add_action( 'wp_enqueue_scripts', 'my_custom_inline_style' );
- Use classes instead of inline
style
attributes: Instead of<div style="color:red;">
, use<div class="my-red-text">
and definemy-red-text
in your external stylesheet.
Less Recommended Solutions (Use with Caution)
4. unsafe-inline
(Avoid if Possible)
Adding 'unsafe-inline'
to your script-src
or style-src
directive will allow all inline scripts and styles. This essentially defeats a major purpose of CSP and opens your site to XSS vulnerabilities. Only use this as a temporary measure during development or if absolutely necessary for a legacy system where refactoring is not feasible.
Example: Content-Security-Policy: script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';
5. unsafe-hashes
(for inline style attributes)
For inline style
attributes (e.g., <div style="width:100%;">
), CSP Level 3 supports unsafe-hashes
. This keyword, combined with hashes, can allow specific inline style attributes. However, it’s generally better to refactor to use classes.
Example: style-src 'self' 'unsafe-hashes' 'sha256-nMxMqdZhkHxz5vAuW/PAoLvECzzsmeAxD/BNwG15HuA=';
General Tips for Implementing CSP in WordPress
- Start with Report-Only Mode: Begin by deploying your CSP with
Content-Security-Policy-Report-Only
header. This will report violations to a specified URI without blocking them, allowing you to identify all problematic inline elements. - Monitor Violations: Set up a reporting endpoint (e.g., using a service or a custom script) to collect CSP violation reports. This is crucial for identifying what’s being blocked.
- Work Iteratively: Fix issues incrementally. Start with core WordPress, then themes, then plugins.
- Test Thoroughly: After each change, test your site extensively, especially dynamic functionalities, forms, and third-party integrations.
- Consider a CSP Plugin: Several WordPress plugins can help manage CSP, generate nonces, and log violations. These can simplify the process, especially for non-developers. Search the WordPress plugin repository for “Content Security Policy”.
- Review Third-Party Code: Plugins and themes often introduce inline scripts and styles. You may need to contact developers or find workarounds if their code isn’t CSP-friendly.
Fixing CSP inline script and style issues in WordPress requires a methodical approach. Prioritizing nonces and refactoring code to use external files or WordPress’s enqueuing functions is the most secure and maintainable long-term strategy.