How to use FacetWP with a Strict Content Security Policy (CSP)
In FacetWP v4.5+, all inline scripts are added with WP’s wp_print_inline_script_tag() function. The same is true for add-ons that load inline scripts: Elementor v1.9.4+, Beaver Builder v1.5+, Bricks v0.7.1+, Multilingual v1.0.2+, and Flatsome v0.4.6+.
This makes it possible to use FacetWP on sites that use a Strict Content Security Policy (CSP) to mitigate cross-site scripting (XSS). For ways to accomplish this, see the implementation section below.
What is Strict CSP?
Cross-site scripting (XSS), the ability to inject malicious scripts into a web app, is one of the biggest web security vulnerabilities. Content Security Policy (CSP) is an added layer of security that helps to mitigate XSS.
To configure a CSP, a Content-Security-Policy HTTP header needs to be added to a web page, and values need to be set that control what resources the browser can load for that page.
With a “nonce-based” CSP, a random number is generated when the page loads, then included in the Content-Security-Policy HTTP header, and then associated with every <script> tag on the page, with a nonce HTML attribute containing the same random number. An attacker then can’t include or run a malicious script in the page, because they would need to guess the correct random number for that script.
To learn more about Strict CSP, here is an article on web.dev with a good explanation.
Adding the “nonce” attribute to scripts in WordPress
In the WordPress context, any Strict CSP implementation will need to add the nonce="{random nonce value}" attribute to two kinds of scripts: linked scripts and inline scripts.
Both script types can be loaded in the HTML <head> or the <body> (mostly in the footer at the bottom) of the page.
Linked scripts
Linked scripts are the standard <script> tags that have a src attribute that points to an internal or external URL. Most themes and plugins will add linked scripts with the wp_enqueue_script() function.
To add a nonce attribute to these scripts, any Strict CSP implementation will use the wp_script_attributes filter hook that automatically runs for scripts added with wp_enqueue_script().
However, this does not work for FacetWP’s linked scripts, because FacetWP intentionally does not use WP’s enqueue or dequeue functions. FacetWP’s assets are only loaded when facets are detected on the page, which cannot be done using wp_enqueue_script() (or wp_enqueue_style() for styles). To add a nonce attribute to FacetWP’s linked scripts, you’ll have to use the facetwp_asset_html hook, as explained below.
Inline scripts
Inline <script> tags do not have a src attribute, but contain JavaScript code embedded directly within the <script> tags. Most themes and plugins will add inline scripts with the wp_print_inline_script_tag() function.
To add a nonce attribute to inline scripts, any Strict CSP implementation will use the wp_inline_script_attributes filter hook. This also works for FacetWP’s (v4.5+) inline scripts, as these scripts are added with this function.
Implement a Strict CSP
The easiest way to implement a Strict Content Security Policy (CSP) on the frontend and login screen of your site is to use a plugin like Strict CSP, or this mu-plugin. Or you could start with the boilerplate code below.
As explained above, these implementations will work out of the box with FacetWP’s inline scripts, but not with FacetWP’s linked scripts, because these scripts are intentionally not added with the standard wp_enqueue_script() function. To fix this, you need a bit of custom code:
Add the “nonce” attribute to FacetWP’s linked scripts
To add the nonce attribute to FacetWP’s linked scripts, you can use the facetwp_asset_html hook. The exact code to get the nonce value depends on the implementation you are using:
If you are using the Strict CSP plugin, add the following to your (child) theme’s functions.php:
How to use custom PHP code?
PHP code can be added to your (child) theme's functions.php file. Alternatively, you can use the Custom Hooks add-on, or a code snippets plugin. More info
add_filter('facetwp_asset_html', function( $html, $url ) { // Only modify <script src="..."> tags, not <link> CSS tags if ( strpos( $html, '<script') !== false ) { $nonce = \StrictCSP\get_nonce(); $html = str_replace('<script ', '<script nonce="' . esc_attr($nonce) . '" ', $html); } return $html; }, 10, 2);
Or, if you are using the above-mentioned mu-plugin, add the following to your (child) theme’s functions.php:
How to use custom PHP code?
PHP code can be added to your (child) theme's functions.php file. Alternatively, you can use the Custom Hooks add-on, or a code snippets plugin. More info
add_filter('facetwp_asset_html', function( $html, $url ) { // Only modify <script src="..."> tags, not <link> CSS tags if ( strpos( $html, '<script') !== false ) { $nonce = get_nonce(); $html = str_replace('<script ', '<script nonce="' . esc_attr($nonce) . '" ', $html); } return $html; }, 10, 2);
Or, if you are using the boilerplate code below, add the following to your (child) theme’s functions.php:
How to use custom PHP code?
PHP code can be added to your (child) theme's functions.php file. Alternatively, you can use the Custom Hooks add-on, or a code snippets plugin. More info
add_filter('facetwp_asset_html', function( $html, $url ) { // Only modify <script src="..."> tags, not <link> CSS tags if ( strpos( $html, '<script') !== false ) { $nonce = get_csp_nonce(); $html = str_replace('<script ', '<script nonce="' . esc_attr($nonce) . '" ', $html); } return $html; }, 10, 2);
Strict CSP boilerplate
Instead of using one of the above-mentioned plugins, you could also implement Strict CSP manually, which gives you a bit more control.
To do so, you could start with the following boilerplate code, which has a lot of explanatory comments and examples for directives to set:
How to use custom PHP code?
PHP code can be added to your (child) theme's functions.php file. Alternatively, you can use the Custom Hooks add-on, or a code snippets plugin. More info
<?php /** * Gets CSP nonce. * * @return string */ function get_csp_nonce(): string { static $nonce = null; if ( $nonce === null ) { // Use a more specific nonce action for CSP to avoid conflicts, if desired. // For general CSP, 'csp' is fine. $nonce = wp_create_nonce( 'csp_nonce_action' ); } return $nonce; } /** * Gets Strict CSP header value. * * @return string */ function get_csp_header_value(): string { // Initialize an array to hold all CSP directives. $directives = []; // Default sources // 'self' allows resources from the same origin. // 'unsafe-inline' is often needed for inline styles generated by themes/plugins. // 'unsafe-eval' might be needed if old scripts or some libraries use eval(). Try to avoid if possible. $directives[] = "default-src 'self'"; // Script sources // script-src-elem for <script> elements. // script-src for workers, eval(), etc. (consider if you need to split this). // For WordPress, 'self' is often needed in addition to nonce for some internal scripts. // Check if any scripts are loaded from CDNs or external domains (e.g., Google Analytics, jQuery CDN). $script_src_elem_sources = [ sprintf( "'nonce-%s'", get_csp_nonce() ), "'self'", // Add any specific external script domains here, e.g.: // 'https://ajax.googleapis.com', // 'https://unpkg.com', ]; $directives[] = "script-src " . implode( ' ', $script_src_elem_sources ); $directives[] = "script-src-elem " . implode( ' ', $script_src_elem_sources ); // Style sources // 'unsafe-inline' is frequently necessary in WordPress due to inline styles. // Add any specific external stylesheet domains here (e.g., Google Fonts, Font Awesome CDN). $style_src_elem_sources = [ "'self'", "'unsafe-inline'", // Unfortunately, inline CSS is not currently filterable easily. 'https://fonts.googleapis.com',// Example for Google Fonts ]; $directives[] = "style-src " . implode( ' ', $style_src_elem_sources ); $directives[] = "style-src-elem " . implode( ' ', $style_src_elem_sources ); // Font sources // 'self' for fonts hosted on your server. // 'data:' for base64 encoded fonts (often used by icon fonts or SVGs). // Add any specific external font domains (e.g., fonts.gstatic.com for Google Fonts). $directives[] = "font-src 'self' data:" . " https://fonts.gstatic.com" . // Example for Google Fonts " https://unpkg.com" . // Example for unpkg ""; // Image sources // 'self' for images on your server. // 'data:' for base64 encoded images. // secure.gravatar.com is good for Gravatars. // Add any other external image sources (e.g., CDN for media library, social media icons). $directives[] = "img-src 'self' data: secure.gravatar.com" . // " https://your-cdn.com" . // Example for a CDN " https://maps.googleapis.com" . // Example for Google Maps " https://maps.gstatic.com" . // Example for Google Maps ""; // Connect sources (for AJAX, WebSockets, EventSource) // 'self' for AJAX requests to your own domain. // Add any external API endpoints or analytics services that your site communicates with. $directives[] = "connect-src 'self'" . " https://www.google-analytics.com" . // Example for Google Analytics " https://maps.googleapis.com" . // Example for Google Maps " https://fonts.googleapis.com" . // Example for Google Fonts // " https://api.example.com" . // Example for an external API ""; // Frame sources (for iframes loaded *by* your page) // 'none' is good if you don't intend to embed any iframes. // If you embed YouTube, Vimeo, Google Maps, etc., you'll need to whitelist them. $directives[] = "frame-src 'none'" . // " https://www.youtube.com" . // Example for YouTube embeds // " https://player.vimeo.com" . // Example for Vimeo embeds ""; // Frame ancestors (who can embed *your* page in an iframe) // 'none' prevents your site from being embedded, good for security. // If you expect your site to be embedded (e.g., in a portal), whitelist the domains. $directives[] = "frame-ancestors 'self'"; // Or 'none' if you absolutely don't want your site embedded // Manifest sources (for web app manifests) $directives[] = "manifest-src 'self'"; // Object sources (for <object>, <embed>, <applet> tags - often for Flash/Java) // 'none' is good as these are usually not needed and can be security risks. $directives[] = "object-src 'none'"; // Base URI (prevents injection of base tags that can redirect relative URLs) // 'none' is a strong security measure. $directives[] = "base-uri 'none'"; // Form Action (where forms can submit data) // 'self' is usually sufficient. $directives[] = "form-action 'self'"; // Upgrade insecure requests (if your site is HTTPS, but some resources are HTTP) $directives[] = "upgrade-insecure-requests"; // Report URI (for violation reports) // Use `report-uri` for older browsers and `report-to` for newer ones. // Ensure your reporting endpoint is correctly configured to receive reports. $directives[] = "report-uri https://yourdomain.com"; // $directives[] = "report-to default"; // Requires a Report-To header as well. // Join all directives with a semicolon and space. return join( '; ', $directives ); } /** * Adds nonce attribute to script attributes. */ foreach ( [ 'wp_script_attributes', 'wp_inline_script_attributes' ] as $hook ) { add_filter( $hook, function( array $attributes ): array { $attributes['nonce'] = get_csp_nonce(); return $attributes; } ); } /** * Sends Strict CSP header. * Applies CSP to all frontend pages and the login screen. * * It's generally better to use 'wp_headers' for the frontend as it's a more standard WordPress hook for headers. * For the login screen, `login_init` is appropriate. */ add_action( 'login_init', function() { header( sprintf( 'Content-Security-Policy: %s', get_csp_header_value() ) ); } ); /** * Send the header on the frontend. */ add_filter( 'wp_headers', static function( $headers ) { // Consider if you want to use Report-Only mode during debugging. // $headers['Content-Security-Policy-Report-Only'] = get_csp_header_value(); $headers['Content-Security-Policy'] = get_csp_header_value(); return $headers; } ); /** * Optional: Add a Report-To header if you want to use the newer reporting API. * This requires more setup on your server to handle the reports. */ add_action( 'send_headers', static function() { header( 'Report-To: {"group":"default","max_age":2592000,"endpoints":[{"url":"https://yourdomain.com/report-to-endpoint"}],"include_subdomains":true}' ); } );