Content-Security-Policy
is a security standard that has been widely adopted by web browsers since 2015. It allows setting an HTTP header that whitelists resources that scripts are allowed to be loaded from.
The Problem
An intruder might add to a page through SQL injection (or another vulnerability) some scripts (or external script files) with malicious code.
The Solution
By adding the right HTTP headers, you can be sure that only scripts from a whitelist will be loaded and run on a page. To add headers in WordPress, developers can use the send_headers
hook by adding the code below to the function.php
of a given theme:
add_action( 'send_headers', 'add_csp_headers' ); function add_csp_headers() {
header( "Content-Security-Policy: default-src 'self' ");
}
There are other headers, too, that can be used to configure CSP.
Configuring CSP for WordPress is quite challenging because of the plugins that need inline JS, third-party libraries, etc. Even WordPress core code has some inline JS code. At the time of this writing, the discussion about making WordPress CSP-friendly isn’t finished yet.
In my opinion, every website owner should find a balance that is appropriate in their situation between security and difficulty of maintenance. How dangerous would it be if some XSS attacks were to happen? The answer is different for a personal blog and for a complex service built on WordPress.
To make your website CSP-friendly, move as much as possible usage of inline JS and CSS to external files, and add a nonce
part to <script>
tags that can’t be made external. The nonce
is a random string that should be unique for each request and coincide with the one mentioned in the CSP header.
Some articles suggest adding an unsafe-inline
header. Don’t do it! It gives the appearance of using CSP but disables protection from running injected scripts. (The reason that some suggest it is because it easily makes CSP “work” with WordPress.)
Changing the way WordPress renders scripts is possible through the script_loader_tag
filter:
add_filter( 'script_loader_tag', 'add_nonce_to_script_tag', 10, 3 );
function add_nonce_to_script_tag( $tag, $handle, $src ) {
$nonce = bin2hex(random_bytes(32));
$replace = sprintf("javascript' nonce='%s' ", $nonce );
$tag = str_replace( "javascript'", $replace, $tag);
return $tag;
}
Do not use wp_create_nonce()
to generate a nonce, as it generates the same nonce for at least 12 hours (24 hours, by default).
The filter for script_loader_tag
would work for everything, except, unfortunately, for the scripts added by wp_localize_script()
and HTML parts where WordPress hard-coded a <script>
tag. The solution might be in filtering all code generated by WP before outputting until the core WP team finds a proper solution.
function callback($buffer) {
$nonce = bin2hex(random_bytes(32));
$replace = sprintf("<script nonce='%s' ", $nonce );
$buffer = preg_replace( "/<script/", $replace, $buffer);
return $buffer;
}
function buffer_start() { ob_start("callback"); }
function buffer_end() { ob_end_flush(); }
add_action('after_setup_theme', 'buffer_start');
add_action('shutdown', 'buffer_end');
This won’t protect from stored XSS, but it helps to prevent DOM-based XSS.