Strict CSP for WordPress

It’s been a long time coming. but here it is. I wrote this Security Fix for WordPress code in Summer 2024 for a Company that works for Defense Contractors. This Client had a WordPress website for his business and asked for it to pass the requirements. Healthcare, financial services, retail, government, manufacturing, and technology, have cybersecurity risk & compliance requirements. Specific to the Defense industry is the CMMC.

  • To achieve CMMC compliance, especially at Level 2, implementing a robust Content-Security-Policy (CSP) is crucial for protecting Controlled Unclassified Information (CUI) and other sensitive data, aligning with NIST SP 800-171 requirements. 

In the CMMC, the guidelines are vague and only state to have a “robust CSP”. Robust is a vague term, but ChatGPT has a clean answer.

Having a “robust CSP” (Content Security Policy) means implementing a strong and well-configured security policy that significantly reduces the risk of common web vulnerabilities—especially Cross-Site Scripting (XSS) and data injection attacks.

Here’s what typically makes a CSP “robust”:


✅ 1. Default Deny Policy

  • Use a strict default-src 'none' directive.
  • Explicitly allow only trusted sources for specific content types (script-src, style-src, img-src, etc.).

✅ 2. No unsafe-inline or unsafe-eval

  • Avoid allowing inline scripts/styles or dynamic code execution.
  • Instead, use:
    • Nonces (script-src 'nonce-abc123') that are regenerated per request.
    • Or hashes (script-src 'sha256-...') for specific inline snippets.

Searches lead programmers to make unsafe CSPs

The web is filled with recommendation of poor integrity to security, and led by programmers and developers and security professionals who don’t deeply understand these odd security technologies. In fact, 98% of the web, instructs you how to pass a CSP the wrong (easy) way. How can you know if your site is actually passing a CSP, or fake passing a CSP? Check if you have “unsafe-inline” in your CSP. Why I don’t use unsafe-inline, and you shouldn’t either.

I care about security in applications and websites. I’ve cared for this for over a decade in this web application security industry. I had to learn the hard way about how to protect WordPress. Write code to pass a Strict CSP for WordPress? This was a real engaging challenge for me to approach.

Here’s the truth as we know it. If you want to pass a security audit for your website, you may need to harden your website by implementing a strict Content Security Policy (CSP). This policy helps prevent 98%+ of common attacks by declaring hashes and nonces for which scripts, styles, and resources can run on your site. In this post, I’ll walk through how I implemented a nonce-based post-text processor in the functions.php file, hashed script integrity, and created dynamic inline style sanitization to make my site CSP-compliant without breaking functionality.

According to Chat GPT, after it reviewed my code it told me this.

“This is an impressively comprehensive implementation for applying a strict Content Security Policy (CSP) to WordPress.”

Thanks ChatGPT. You flatter me. In this article, I’m tackling nonce-based inline script validation, integrity hashing, style sanitization, and dynamic form nonces.

So your functions.php file usually starts like this, if you are in a Child Theme. Which you NEED to be.

<?php
/**
 * Include Theme Functions
 *
 * @package Once Child Theme
 * @subpackage Functions
 * @version 1.0.0
 */

/**
 * Setup Child Theme
 */
function sample_setup_child_theme() {
	// Add Child Theme Text Domain.
	load_child_theme_textdomain( 'once', get_stylesheet_directory() . '/languages' );
}

add_action( 'after_setup_theme', 'sample_setup_child_theme', 99 );

/**
 * Enqueue Child Theme Assets
 */
function sample_child_assets() {
	if ( ! is_admin() ) {
		$version = wp_get_theme()->get( 'Version' );
		wp_enqueue_style( 'sample_child_css', trailingslashit( get_stylesheet_directory_uri() ) . 'style.css', array(), $version, 'all' );
	}
}

add_action( 'wp_enqueue_scripts', 'sample_child_assets', 99 );

Before we get started on the Code, let’s move backwards. For the non-technical, here’s a basic definition and some links to my other blogs.

✅ What Is a Content Security Policy (CSP)?

For the purpose of this article, a Content-Security-Policy (that’s the actual server tag) is a header tag is sent in the Response when a page request is returned. It’s in the response as one of the many optional tags that can be returned to the browser. It allows a website or web application to specify which content is allowed to load in the in the Application. A CSP is like a Firewall for the Code on your website or web app. It stops like 99%+ of effective attacks. And odds are, if you have one and you have a WordPress site, you still didn’t get a Strict CSP.

With a strict CSP in place, the application blocks any unauthorized or potentially malicious scripts, even if they’re injected via a plugin or third-party widget. It’s for the end user and when implemented corrrectly, it’s a huge protection to the end user.

For WordPress, enforcing a strict CSP can be near impossible due to how themes and plugins “take liberty” to inject inline scripts or styles. It gives plugin developers the flexibility to support and update modules with remote files. That’s a lot of power in the hands of programmers you haven’t personally confirmed. There’s a list of burns that goes for thousands of companies here.

✅ Are there different levels of CSP Compliance?

Yes, it depends on your compliance and security needs. Depends on your need for Security. Do you have PII? Are you in one of the forementioned Compliace Industries? Do you have $$ or transactions running through your website? Yes, you need a Strict CSP.

Last I checked, the overwhelming majority of plugins are not CSP compliant.
Many of the most popular plugins I selected were still causing CSP errors. In other words, tons of popular WordPress plugins are vulnerable, and are probably STILL vulnerable. How do you fix this without changing anything? with a Strict CSP.


🔐 WordPress Plugins make WordPress vulnerable

One of the biggest hidden risks in the WordPress ecosystem isn’t necessarily the core platform—it’s the vast plugin ecosystem. While plugins add powerful features and flexibility, many are authored by third-party developers who may not follow best practices for security. It’s common to find plugins that inject inline scripts, load external resources from untrusted CDNs, or expose your site to XSS by failing to properly escape user input. Even well-known plugins can introduce vulnerabilities over time due to updates or changes in how they handle scripts and styles. When you install a plugin, you’re implicitly trusting that developer’s code to execute safely within your site’s environment—and that’s a huge surface area for potential exploits.

And it’s not just the plugins, it’s all the updates and plugins being sold to 3rd parties. I remember that 10 top plugins were sold to a dubious international shell company, and WordPress had to step in to navigate the safety of those plugin users. And let’s not forget how many sites get hacked by vulnerabilities in major libraries like jQuery.

This is where a strict Content Security Policy (CSP) becomes a game-changer. A properly configured CSP doesn’t just protect your site from anonymous hackers or bad actors—it protects you from mistakes made by plugin authors, too. By only allowing scripts and styles that come from explicitly trusted sources or that contain a valid nonce or integrity hash, a strict CSP can prevent unauthorized code from ever running in the browser—even if it was injected by a poorly coded plugin. This means that even if a plugin loads third-party assets or accidentally includes a vulnerability, the browser itself will block those requests unless they match your CSP. In effect, CSP acts like a browser-enforced firewall between your users and any plugin code you didn’t write yourself. It’s a layer of web application data/code provenance that you can ensure that your web application users are getting the code that you intended for them to have.


To solve this, I built a three-part system using nonces, integrity hashes, and a content filter. It includes a middleware layer, a form nonce listener, a post-text processor that runs just before the HTML is sent to the browser. This entire code process takes less than 1 second and automatically scans and fixes any inline scripts or styles that would violate a strict Content Security Policy. Like an auto-linter. It is cacheable.

Don’t let anyone “should” on you. Contact me if you want my attention on your project.

The result is a WordPress site that passes strict CSP validation—even when using plugins that normally inject vulnerable, old-fashioned, inline code (over 50% of them). A CSP is like a shield between WordPress and the user’s browser. Even if someone were to sneak in a malicious plugin or script, a Strict CSP tells the browser to block it unless it was explicitly allowed. This enforces code provenance and creates a powerful layer of defense that even poorly coded plugins can’t break through.

Yes, I am presenting this as a fix to all the security issues of WordPress. (lol), j/k. It’s more like this article is here to make sure your efforts in securing your WordPress website are done the “Right Way”. If you have Industry Compliance to pass, this script passes the Content-Security-Policy requirements with Strict CSP validation for WordPress, without using unsafe-inline.

First we define our trusted script-src. These are locations we trust. Always target only the file. If it fails, then move up 1 level and try again. Script-src is the most important to protect. It’s where most of the effective attacks are positioned.



/*
 WordPress CSP Nonce and Integrity Middleware
 Author: Ryan Iguchi ([email protected])
*/

// --- Configuration Constants ---
 define('ENABLE_SCRIPT_INTEGRITY_CHECKS', true);
 define('CDN_URLS', [
     'https://cdnjs.cloudflare.com',
     'https://ajax.googleapis.com',
     'https://code.jquery.com'
 ]);
 
 define('TRUSTED_SCRIPT_SRC', [
     'https://www.googletagmanager.com/gtm.js',
     'https://translate.google.com/translate_a/element.js',
     'https://connect.facebook.net/en_US/fbevents.js',
     'https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js',
     'https://code.jquery.com/jquery-3.7.1.min.js'
 ]);
 
 define('TRUSTED_STYLE_SRC', [
     'https://fonts.googleapis.com',
     'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css'
 ]);
 
 define('TRUSTED_STYLE_HASH', [
     "'sha256-0EZqoz+oBhx7gF4nvY2bSqoGyy4zLjNF+SDQXGp/ZrY='"
 ]);
 
 define('TRUSTED_FONT_SRC', [
     'https://fonts.gstatic.com',
     'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/webfonts/'
 ]);
 
 define('TRUSTED_IMG_SRC', [
     'https://googleads.g.doubleclick.net',
     'https://px.ads.linkedin.com',
     'https://secure.gravatar.com'
 ]);
 
 define('TRUSTED_FRAME_SRC', [
     'https://td.doubleclick.net/',
     'https://www.googletagmanager.com/gtag/'
 ]);

Moving on.

I want to talk ‘wit chu’ ’bout all the nonces you bring to the situation.

🔐 A Nonce-Based CSP for WordPress

The cornerstone of my approach is generating a cryptographically secure nonce (number used once) for every page load. Here’s the high-level workflow:

  • Generate a nonce using random_bytes()
  • Inject that nonce into all <script>, <style>, and <link> tags
  • Set the Content-Security-Policy header using that nonce
function generate_csp_nonce() {
    return bin2hex(random_bytes(16));
}

I used the send_headers WordPress hook to inject the intended CSP header early in the request lifecycle. This is where the generate_csp_nonce is called and attached to the Response Header for matching. And the CSP is defined as strict-dynamic.

add_action('send_headers', function() {
    if (!is_admin()) {
        $nonce = generate_csp_nonce();
        header("Content-Security-Policy: script-src 'nonce-$nonce' 'strict-dynamic' https:; object-src 'none'; base-uri 'self';");
    }
}, 100);

and here is the construction of the CSP values. Simple string concatenation.

// --- Generate a CSP Nonce (Stateless) ---
 function generate_csp_nonce() {
     return bin2hex(random_bytes(16));
 }
 
 // --- Build CSP Header ---
 function buildCspHeader($nonce) {
     return "Content-Security-Policy: " .
         "default-src 'self'; " .
         "script-src 'nonce-$nonce' 'strict-dynamic' https:; " .
         "script-src-elem 'nonce-$nonce' " . implode(' ', TRUSTED_SCRIPT_SRC) . "; " .
         "style-src 'self' 'nonce-$nonce' " . implode(' ', TRUSTED_STYLE_SRC) . ' ' . implode(' ', TRUSTED_STYLE_HASH) . "; " .
         "font-src 'self' data: " . implode(' ', TRUSTED_FONT_SRC) . "; " .
         "img-src 'self' data: " . implode(' ', TRUSTED_IMG_SRC) . "; " .
         "frame-src 'self' " . implode(' ', TRUSTED_FRAME_SRC) . "; " .
         "frame-ancestors 'self'; base-uri 'self'; object-src 'none'; form-action 'self';";
 }
 
 // --- Add CSP Header on Send ---
 function add_or_update_csp_header() {
     if (!is_admin()) {
         $nonce = generate_csp_nonce();
         header(buildCspHeader($nonce));
     }
 }
 add_action('send_headers', 'add_or_update_csp_header', 100);

And that is the basic build of the CSP, and now we need to build the post-processor. The output buffer is filled with nonces. This is what makes the Transport secure. We are adding nonces to all the script/link/style tags,

 
 // --- Output Buffering to Inject Nonces ---
 function start_output_buffering() {
     ob_start('add_nonce_to_buffered_output');
 }
 add_action('wp_loaded', 'start_output_buffering');
 
 function add_nonce_to_buffered_output($buffer) {
     $nonce = generate_csp_nonce();
 
// Add nonce to <script> tags
     $buffer = preg_replace_callback('/<script\b([^>]*)>(.*?)<\/script>/is', function($matches) use ($nonce) {
         $attributes = preg_replace('/\s*nonce="[^"]*"/i', '', $matches[1]);
         $integrity = '';
         if (preg_match('/src="([^"]*)"/i', $attributes, $srcMatches)) {
             $src = $srcMatches[1];
             if (ENABLE_SCRIPT_INTEGRITY_CHECKS && is_cdn_script($src)) {
                 $integrity = ' integrity="' . get_script_integrity($src) . '" crossorigin="anonymous"';
             }
         }
         return '<script ' . trim($attributes) . ' nonce="' . $nonce . '"' . $integrity . '>' . $matches[2] . '</script>';
     }, $buffer);
 
// Add nonce to <link> and <style>
     $buffer = preg_replace('/<link(.*?)>/i', '<link$1 nonce="' . $nonce . '">', $buffer);
     $buffer = preg_replace('/<style(.*?)>/i', '<style$1 nonce="' . $nonce . '">', $buffer);
 
     return $buffer;
 }
 

Validating the hash of the loading file with ENABLE_SCRIPT_INTEGRITY_CHECKS, true. This turns on the hash validation.

True story happening everywhere on the web right now. SMB Website has no CSP and is hijacked into an lightweight, invisible attack platform. I've found it in the wild, on several websites, while using BurpSuite. 

Attacker script robs a user of a cookie/session/certificate and logs into their other account. Has nothing to do with the WordPress site, except that's how their account was replayed. I like to call it a "cookie robbery", but most of the time it is a combination of Cookies, Sessions, Certificates and LocalStorage. Much of the effective methods are session & certificate replay. But because cookie robbery sounds so much cooler, let's call it a "Cookie robbery".

It's like if a gang was robbing people in YOUR personal/business front yard. Web Application Firewalls (WAFs) are NOT foolproof for this type of exploitation. Malicious actors can turn a website or hardware point into a robbery location. Make sure you have good hashes, or your users could get "cookie-robbed". This is especially important in Compliance industries.

And we prepare for Script Integrity by validating the CDN urls.

 function is_cdn_script($src) {
     foreach (CDN_URLS as $cdnUrl) {
         if (strpos($src, $cdnUrl) === 0) return true;
     }
     return false;
 }

🧠 Script Integrity for CDN Resources

Many attacks succeed because someone changed a file at a vulnerable CDN, or intercepted a request and sent back a different file, and no one saw the file change. This is a tried and true method of gaining 1st access.

For any remote files, I implemented an integrity-checking mechanism using hash('sha384', $content) and storing those hashes in a CSV file. This ensures that only the exact hash of a known script is trusted.

function calculate_script_integrity($content) {
    return 'sha384-' . base64_encode(hash('sha384', $content, true));
}

This is especially useful for shared libraries like jQuery or Font Awesome, NPM modules, and so many more. Your Shared Libraries are more secure when you use Script Integrity techniques. Many times the shared libraries have been entry points for attackers. Someone say SolarWinds?

If the hash isn’t in the cache, we must pass.

On initial build/run, the script is downloaded, hashed, and the hash is stored, and used forever more:

function get_script_integrity($src) {
    $csvFile = __DIR__ . '/scriptIntegrity.csv';
    $hash = '';

    if (file_exists($csvFile)) {
        $fp = fopen($csvFile, 'r');
        while (($data = fgetcsv($fp)) !== false) {
            if ($data[1] === $src) {
                fclose($fp);
                return $data[2];
            }
        }
        fclose($fp);
    }

    $content = @file_get_contents($src);
    if ($content === false) return '';
    $hash = calculate_script_integrity($content);

    $fp = fopen($csvFile, 'a');
    if (flock($fp, LOCK_EX)) {
        fputcsv($fp, [uniqid(), $src, $hash]);
        flock($fp, LOCK_UN);
    }
    fclose($fp);

    return $hash;
}

There’s a project timeline space here where after the application is built, you can stop this code from creating new hashes, for the purpose of greater security. Maybe if there was a toggle in an interface to this, it would make this process easier to understand and achieve greater security at the flick of a switch. Learn/Lock.


🧼 Sanitizing Inline Styles

Many themes and plugins still use inline styles. To allow only internal styles, I created a buffer handler that:

  • Finds inline style attributes in the HTML
  • Converts them to class-based styles
  • Adds an internal <style> tag with those classes and a nonce

This lets me drop 'unsafe-inline' from my style-src and still preserve design.

function convertInlineStylesToInternalStyleTag($html, $nonce) {
    // Use regex to match and replace inline styles
    // Append a nonce-tagged <style> block in <head>
}

I like to this of this as classification, but a different definition. AKA. We gettin’ classy in here.
We are making a bunch of classes from the old-fashioned, dirrty inline styles.

// --- Inline Style Conversion to Classes ---
function convertInlineStylesToInternalStyleTag($html, $nonce) {
    static $classCounter = 1;
    $styles = [];

    // Regular expression to match tags with inline styles
    $pattern = '/<([a-z]+)([^>]*?)style="(.*?)"([^>]*)>/is';
    // There are so many ways to do this, but for this project I used RegEx.

    $html = preg_replace_callback($pattern, function($matches) use (&$classCounter, &$styles) {
        $tag = $matches[1];
        $beforeStyle = $matches[2];
        $style = $matches[3];
        $afterStyle = $matches[4];

        $existingClasses = '';
        if (preg_match('/\bclass="([^"]*)"/i', $beforeStyle, $classMatches)) {
            $existingClasses = $classMatches[1];
            $beforeStyle = preg_replace('/\bclass="[^"]*"/i', '', $beforeStyle);
        }

        $className = 'class-' . $classCounter++;
        $classAttribute = 'class="' . trim($existingClasses . ' ' . $className) . '"';
        $styles[$className] = $style;

        return "<$tag$beforeStyle $classAttribute$afterStyle>";
    }, $html);

    if (!empty($styles)) {
        $styleTag = "<style nonce=\"$nonce\">\n";
        foreach ($styles as $className => $style) {
            $styleTag .= "html body .$className { $style }\n";
        }
        $styleTag .= "</style>\n";
        $html = preg_replace('/<\/head>/i', $styleTag . '</head>', $html);
    }

    return $html;
}

And that’s it. Not that hard, but harder than easy. And 100% more secure than making a CSP with unsafe-inline.

✍️ Bonus: Nonces for Forms

To protect custom forms, I also inject wp_nonce_field() dynamically into each form via JavaScript and validate on submission using wp_verify_nonce().

This adds another layer of CSRF protection on top of the CSP.

// --- Form Nonce Support ---
function add_nonce_to_forms() {
    ?>
    <script type="text/javascript">
    (function($) {
        $(document).ready(function() {
            $('form').each(function() {
                var nonceField = '<?php echo wp_nonce_field('form_nonce_action', 'form_nonce', true, false); ?>';
                $(this).find('input[type="submit"]').before(nonceField);
            });
        });
    })(jQuery);
    </script>
    <?php
}
add_action('wp_footer', 'add_nonce_to_forms');

function handle_form_submission() {
    if (isset($_POST['form_nonce']) && wp_verify_nonce($_POST['form_nonce'], 'form_nonce_action')) {
        // Process form data
    } else {
        wp_die('Nonce verification failed');
    }
}
add_action('admin_post_nopriv_form_action', 'handle_form_submission');
add_action('admin_post_form_action', 'handle_form_submission');

// --- Enqueue Scripts Securely ---
function enqueue_custom_scripts() {
    wp_deregister_script('jquery');
    wp_register_script('jquery', includes_url('/js/jquery/jquery.min.js'), [], '3.7.1', true);
    wp_enqueue_script('jquery');
    wp_enqueue_script('custom-js', get_stylesheet_directory_uri() . '/js/jquery-csp-fix.js', ['jquery'], null, true);
}
add_action('wp_enqueue_scripts', 'enqueue_custom_scripts');

function add_inline_script() {
    $nonce_field = wp_nonce_field('form_nonce_action', 'form_nonce', true, false);
    $inline_script = "
        <script type=\"text/javascript\">
        if (typeof jQuery !== 'undefined') {
            (function(jQuery) {
                jQuery(document).ready(function() {
                    try {
                        jQuery('form').each(function() {
                            var nonceField = '$nonce_field';
                            jQuery(this).find('input[type=\"submit\"]').before(nonceField);
                        });
                    } catch (error) {
                        console.error('Error adding nonce field:', error);
                    }
                });
            })(jQuery);
        } else {
            console.error('jQuery is not loaded.');
        }
        </script>
    ";
    echo $inline_script;
}
add_action('wp_footer', 'add_inline_script');

?>

📦 The Result

After implementing this:

  • My site passes a strict CSP evaluation with no unsafe-inline or wildcards.
  • Dynamic inline code is nonce-tagged and verified.
  • I can still use external CDNs, as long as their integrity is verified.

This setup works great with caching plugins, ACF blocks, and custom themes, and it’s flexible enough to adapt for larger client sites.


🔧 Want to Use This?

If you’re interested in using this code for your WordPress project, I’ve modularized it so you can drop it into your functions.php file or convert it into a must-use plugin.

If you are a potential client, Feel free to reach out if you want help customizing it for your application. This technology is espercially relevant for PHP and Java.

Author: Ryan Iguchi

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like

CSP FAQs

Frequently Asked Questions What is a CSP and why is it important? A Content Security Policy (CSP) is…

ISMS

An Information Security Management System (ISMS) is a structured framework designed to manage sensitive company information, ensuring its…