In FacetWP v4.5+, and add-ons released after that, all inline scripts are added with WP’s wp_print_inline_script_tag() function. This makes it possible to use FacetWP on sites that use a Strict Content Security Policy (CSP) to mitigate cross-site scripting (XSS).

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 at runtime, included in the CSP, and then associated with every <script> tag on the page, with a nonce 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.

The wp_print_inline_script_tag() function that FacetWP (v4.5+) and most plugins and themes use, provides two filter hooks to add a nonce attribute to the inline <script> tag that it prints on the page: wp_script_attributes and wp_inline_script_attributes.

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.

To do this manually, or for more control, 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}' ); } );

See also

Last updated: October 23, 2025