[wp-trac] [WordPress Trac] #39941: Allow using Content-Security-Policy without unsafe-inline
WordPress Trac
noreply at wordpress.org
Mon Apr 15 18:09:23 UTC 2024
#39941: Allow using Content-Security-Policy without unsafe-inline
-------------------------------------------------+-------------------------
Reporter: tomdxw | Owner:
| adamsilverstein
Type: enhancement | Status: closed
Priority: normal | Milestone: 5.7
Component: Security | Version: 4.8
Severity: normal | Resolution: fixed
Keywords: has-patch has-unit-tests commit | Focuses: javascript
has-dev-note |
-------------------------------------------------+-------------------------
Comment (by amanandhishoe):
So for those who happen to read through this dialog, I have managed to
implement a CSP on my frontend using a mu-plugin I wrote which calculates
hashes for all inline scripts and creates a CSP with those hashes.
I used Pascal CESCATO's CSP-ANTS&ST plugin as a starting point to create
the plugin. He added nonces to all inline scripts. I had my mu-plugin
doing the same, but when I discovered how easy it was to calculate hashes
for all inline scripts, I switched to that because with nonces I needed to
modify the html and place the nonces inside the inline scripts. With
hashes, I don't have to touch the final html. Browsers will calculate
hashes on their end for inline scripts and see if those hashes are
included in the CSP. They won't run any scripts which don't generate one
of the hashes in my CSP.
This is the code that creates hashes for all the inline scripts:
{{{
$hashes_a = array(); // array of hashes for inline scripts.
$page_html = preg_replace_callback(
'#<script.*?>(.*?)<\/script>#s',
function ( $matches ) use ( &$hashes_a ) {
$script_content = $matches[1]; // Extract the
content between the script tags.
// phpcs:ignore
$hash = base64_encode( hash( 'sha256',
$script_content, true ) ); // Compute the SHA-256 hash and encode it in
Base64.
$hashes_a[] = "'sha256-" . $hash . "'"; // Add the
hash to the list with the 'sha256-' prefix.
return $matches[0]; // Return the original script
tag unmodified.
},
$page_html
);
// Now $hashes_a contains the hashes for the inline scripts, and
can be included in the CSP header.
// Reduce the array so that each hash only appears once.
$hashes_a = array_unique( $hashes_a );
// Make a string with a list from hashes.
$header = '';
$hashes_csp = array_reduce(
$hashes_a,
function ( $header, $hash ) {
return "{$header} {$hash}"; // Note the space
between the quotes to separate the hashes.
},
''
);
}}}
Once I have that, I make my CSP:
{{{
header( sprintf( "Content-Security-Policy: base-uri 'self' %1s
;object-src 'none';script-src 'self' %2s %3s %4s;style-src 'self' %5s
'unsafe-inline';", $uris, $uris, $extra_script_rules, $hashes_csp, $uris )
);
}}}
In $uris I have a list of allowed sources for scripts.
In $extra_script_rules I have a list of extra script rules if I need any.
So my CSP looks like this and you can see the list of hashes in the
script-src policy:
base-uri 'self' mycdn.rocketcdn.me challenges.cloudflare.com
checkout.clover.com ;object-src 'none';script-src 'self'
mycdn.rocketcdn.me challenges.cloudflare.com checkout.clover.com data:
'sha256-77WmSGVq6PlE+/dOVkQSZGQWCrUBl6KIyLWH507dV1o='
'sha256-M1etSTsLTYyio9eWYa4749eeV1vBu8wcRrvhuWcZKC4='
'sha256-7y9/KNsyJQGWriyCQmEaf3FZwqU52r1AuCBxscB1YcY='
'sha256-hmgfyXY6AzlJeRWqm+bMvB3XPWFuPrZX1muhIi+gtSc='
'sha256-MTQk+ZugiSyMOmP4Z9xCUFM5CCHjf5UIyrv7RIN82oE='
'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='
'sha256-5cm3ZpD+ehJUySErDDMlNpvcRe51l8wF7vpRdvQIU/Y='
'sha256-JifyozaSM89dFYWoPUZeY3Sz3S/t5J/0HY1rFsIxq68='
'sha256-1X+hmeDCSiqR5517YUrA2tiViPVS+yZYRp3WkC1g3vk='
'sha256-A8P10y0WSvNDEFERE8AOaLoSXb/km+njslYccDvOCok='
'sha256-eHL/Izx7K/qWL0kdBXXnHwsLSHvGOJn/THLHydUZdog='
'sha256-C7CIpwSS5m3mXZcXJPPh/8geedELe6rTiB841eQdFZ4=';style-src 'self'
mycdn.rocketcdn.me challenges.cloudflare.com checkout.clover.com 'unsafe-
inline';
And since the CSP is created dynamically on every page, it doesn't care
what inline scripts are in the final html. It will make a hash for it. And
if an inline script changes because a plugin updates, it doesn't matter
because a hash for the new script will be created when that new script
ends up in the final html.
Since I do use WP-Rocket, for pages optimized by WP-Rocket, I have to hook
into the filter 'rocket_buffer' to get the final html which WP-Rocket
generates. For frontend pages not handled by WP-Rocket I hook into the
'template_redirect' action to get the final html.
On the admin side, I hook into 'admin_init' to get the final html this
way:
{{{
add_action(
'admin_init',
/**
* Callback function to modify output before it is sent to the
browser.
*
* @param void
*
* @return string The modified output.
*/
function () {
if ( ! is_admin() ) {
return; // Exit the function if not on an admin
page.....
}
ob_start(
/**
* Callback function to modify output before it is
sent to the browser.
*
* @param string $output The output to be
modified.
*
* @return string The modified output.
*/
function ( $output ) {
global $wp;
$output_length = strval( strlen( $output )
);
make_admin_csp_hashes( $output, '' );
return $output;
}
);
},
PHP_INT_MAX
);
}}}
On the admin side, I did have to add this logic to add 'unsafe-eval' to
the CSP because there are some included javascript files which call things
like Function() which require 'unsafe-eval' in the CSP for the scripts to
run.
{{{
if ( false !== strpos($page_html, 'block-templates/index.js' ) ||
// WooCommerce problem.
false !== strpos($page_html, 'underscore.min.js') ||
false !== strpos($page_html, 'handlebars.min.js')) { //
UpdraftPlus issue.
$extra_script_rules .= " 'unsafe-eval' ";
}
}}}
So there are some pages on the admin side that must have 'unsafe-eval' in
order for them to work. But I can control that page by page and leave
'unsafe-eval' off many admin side pages.
This is a plugin specific to my site. Depending on what optimizing plugin
you use, how you access the final html may vary. And there may be plugins
on your frontend that use javascript files that won't run without 'unsafe-
eval' in your script-src CSP. Fortunately I haven't found any on my
WordPress site. Major plugins I use are Happyforms, Simple Cloudflare
Turnstile, UpdraftPlus, WooCommerce, Wordfence, Wp Mail SMTP Pro, WP-
Rocket, Yoast SEO ( with Premium & WooComerce additions).
--
Ticket URL: <https://core.trac.wordpress.org/ticket/39941#comment:115>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform
More information about the wp-trac
mailing list