Secure WooCommerce Checkout with Content Security Policy Headers and Subresource Integrity

Content Security Policy (CSP) is an HTTP response header that tells the browser which origins are allowed to load scripts, styles, images, and other resources — blocking injected third-party scripts that steal payment card data even if the site is compromised via a cross-site scripting vulnerability or a supply-chain attack against a JavaScript dependency. PCI DSS 4.0 (effective 2025) explicitly requires CSP or equivalent controls on payment pages to prevent skimming attacks, making CSP implementation on WooCommerce checkout pages a compliance requirement, not just a best practice. Implementing CSP on WooCommerce requires careful inventory of every script, style, and third-party resource the checkout page loads — a missing origin in the policy causes the resource to be blocked and the checkout to break. Report-Only mode (Content-Security-Policy-Report-Only) sends violation reports to a designated endpoint without blocking any resources, allowing the policy to be tuned on a live site before enforcement begins. Subresource Integrity (SRI) adds a cryptographic hash to <script> and <link> tags — the browser refuses to execute the resource if its content does not match the expected hash, preventing tampered CDN assets from executing on the payment page. Nonce-based CSP is more robust than allowlist-based CSP because it authorises specific script elements rather than all scripts from a domain — WordPress 6.3+ outputs a CSP nonce for inline scripts when a filter provides one. Payment providers such as Stripe and PayPal serve their JavaScript from fixed CDN origins (js.stripe.com, www.paypal.com) that must be explicitly allowed in the script-src directive alongside your own domain. The frame-ancestors directive in CSP replaces the older X-Frame-Options header and prevents the checkout page from being embedded in an iframe on a phishing site — set it to 'none' unless you have a legitimate use for iframe embedding. The 2FA post covers the admin authentication layer that CSP on the front end complements — together they address both admin account takeover and checkout skimming threats. The security headers post explains the full suite of HTTP security headers that accompany CSP on a hardened WordPress installation — deploy all of them together for defence in depth. Use Content-Security-Policy-Report-Only with a free endpoint like report-uri.com for at least two weeks before switching to enforcement mode — checkout breakage due to a blocked payment script costs more than the time spent tuning the policy.

Problem: WooCommerce checkout pages without a Content Security Policy are vulnerable to JavaScript skimming attacks that inject malicious code to steal payment card data — a single compromised plugin or CDN asset can silently exfiltrate all submitted card numbers without triggering any server-side security controls.

Solution: Add a strict Content-Security-Policy header to WooCommerce checkout pages via PHP using WordPress hooks, start in Report-Only mode to identify violations, then enforce the policy and add Subresource Integrity hashes to third-party script tags.

// Add CSP header to checkout and payment pages only
add_action('send_headers', function() {
    if (!function_exists('is_checkout') || !is_checkout()) return;

    $nonce = base64_encode(random_bytes(16));

    // Store nonce so wp_script_attributes filter can use it for inline scripts
    define('MY_CSP_NONCE', $nonce);

    $policy = implode('; ', [
        "default-src 'self'",
        "script-src 'self' 'nonce-{$nonce}' https://js.stripe.com https://www.paypal.com",
        "style-src  'self' 'unsafe-inline'", // WooCommerce uses inline styles
        "img-src    'self' data: https://www.paypalobjects.com",
        "frame-src  https://js.stripe.com https://www.paypal.com",
        "connect-src 'self' https://api.stripe.com",
        "frame-ancestors 'none'",
        "form-action 'self'",
        "base-uri 'self'",
        "upgrade-insecure-requests",
    ]);

    // Start in report-only mode: replace header name once policy is validated
    header("Content-Security-Policy-Report-Only: {$policy}; report-uri /csp-report");
    // Enforce: header("Content-Security-Policy: {$policy}");

    // X-Frame-Options for older browsers that ignore CSP frame-ancestors
    header('X-Frame-Options: DENY');
});

// Add the CSP nonce attribute to WordPress-enqueued inline scripts
add_filter('wp_inline_script_attributes', function(array $attrs): array {
    if (defined('MY_CSP_NONCE')) {
        $attrs['nonce'] = MY_CSP_NONCE;
    }
    return $attrs;
});

// Add SRI hash to a specific third-party script tag (example: lodash from CDN)
add_filter('script_loader_tag', function(string $tag, string $handle): string {
    if ($handle !== 'my-cdn-lodash') return $tag;
    return str_replace(
        '<script ',
        '<script integrity="sha384-PASTE_HASH_HERE" crossorigin="anonymous" ',
        $tag
    );
}, 10, 2);

NOTE: Generate SRI hashes for third-party scripts at srihash.org or with openssl dgst -sha384 -binary script.js | openssl base64 -A — regenerate the hash every time the third-party script version changes, because the hash binds to the exact bytes of that specific version.