[wp-trac] [WordPress Trac] #39941: Allow using Content-Security-Policy without unsafe-inline
WordPress Trac
noreply at wordpress.org
Sun Aug 10 01:52:42 UTC 2025
#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):
I thought about the problem of making a hash to all inline scripts in the
final html and do see I would be authorizing a malicious script if it
managed to get in there. So I made some changes to my mu-plugin that
creates hashes for inline scripts.
The first thing I did was define a random token to use for the session:
{{{
if ( ! defined('DBTN_CSP_TOKEN') ) {
define('DBTN_CSP_TOKEN', base64_encode(random_bytes(16))); // per-
request.
}
}}}
Then I added a filter to 'wp_inline_script_attributes' so that whenever a
plugin enqueues an inline script, the script will get tagged with data-
owner='dbtn' and data-cspid= DBTN_CSP_TOKEN:
{{{
add_filter('wp_inline_script_attributes', function ($attrs, $handle) {
// front-end only, if desired:
if ( is_admin() ) return $attrs;
$attrs['data-owner'] = 'dbtn';
$attrs['data-cspid'] = DBTN_CSP_TOKEN; // prevents spoofing in your
hash pass
return $attrs;
}, 10, 2);
}}}
Now I can easily identify valid inline scripts plugins enqueue.
Then I ran through all the pages of my site, I have a mirror of my
production site running in a VM on my MacBook Pro. Safari quickly pointed
out the inline scripts that didn't get hashes.
So I figured out which valid inline scripts are being added to web pages
and made a function to tag those:
{{{
function dbtn_tag_known_inline(string $html): string {
$tok = DBTN_CSP_TOKEN;
return preg_replace_callback('#<script\b([^>]*)>(.*?)</script>#is',
function($m) use ($tok) {
$attrs = $m[1]; $body = $m[2];
// Skip external, nonced, non-executing
if (preg_match('#\bsrc\s*=#i', $attrs)) return $m[0];
if (preg_match('#\bnonce\s*=#i', $attrs)) return $m[0];
if
(preg_match('#\btype\s*=\s*["\'](application/(ld\+)?json|text/(template|html))["\']#i',
$attrs)) return $m[0];
// Known signatures
$is_nojs_toggle = (bool) preg_match(
'#className\.replace\(/\s*[^/]*no-
js[^/]*/\s*,\s*[\'"]js[\'"]\s*\)#i',
$body
);
$is_wc_toggle = strpos($body, "woocommerce-no-js") !==
false;
$is_happyforms =
preg_match('#\\bHappyForms\\s*=\\s*\\{\\s*\\};?#', $body);
$is_payeezy_cc = strpos($body,
'wc_first_data_payeezy_gateway_credit_card_payment_form_handler') !==
false;
$is_payeezy_echk = strpos($body,
'wc_first_data_payeezy_gateway_echeck_payment_form_handler') !== false;
$is_rocket_lazy_cfg = (bool)
preg_match('#window\s*\.\s*lazyLoadOptions\s*=\s*\[#', $body);
if ($is_nojs_toggle || $is_wc_toggle || $is_happyforms ||
$is_payeezy_cc || $is_payeezy_echk || $is_rocket_lazy_cfg) {
// Already tagged?
if (!preg_match('#\\bdata-cspid=#i', $attrs)) {
$attrs = rtrim($attrs);
$open = '<script' . ($attrs ? ' ' . $attrs : '') .
' data-owner="dbtn" data-cspid="' . $tok . '">';
return $open . $body . '</script>';
}
}
return $m[0];
}, $html);
}
}}}
And I elaborated the routine where I make csp hashes to make a hash only
for scripts with my data-owner and data-cspid tags or specific scripts.
Any inline script that does not match does not get a csp hash:
{{{
if (
preg_match_all('#<script\b(?P<attrs>[^>]*)>(?P<body>.*?)</script>#is',
$page_html, $m, PREG_SET_ORDER) ) {
foreach ( $m as $s ) {
$attrs = $s['attrs'];
$body = $s['body'];
if ( preg_match('#\bsrc\s*=#i', $attrs) ) continue; //
external
if ( preg_match('#\bnonce\s*=#i', $attrs) ) continue; //
already nonced
// Skip non-executing types (allow rocketlazyloadscript)
if (
preg_match('#\btype\s*=\s*["\'](application/(ld\+)?json|text/(template|html))["\']#i',
$attrs)
&& stripos($attrs, 'rocketlazyloadscript') === false ) {
continue;
}
// Allow: your tagged inlines, Rocket placeholders/loader,
Rocket IE snippet
$tok = preg_quote(DBTN_CSP_TOKEN, '#');
$is_ours = preg_match('#\bdata-owner=["\']dbtn["\']#i',
$attrs)
&& preg_match('#\bdata-
cspid=["\']'.$tok.'["\']#i', $attrs);
$is_rocket_lazy = stripos($attrs, 'rocketlazyloadscript')
!== false;
$is_rocket_loader = stripos($body, 'RocketLazyLoadScripts')
!== false;
$is_rocket_ie = strpos($body, 'nowprocket=1') !== false
&& strpos($body,
'navigator.userAgent.match(/MSIE') !== false;
if ( ! ($is_ours || $is_rocket_lazy || $is_rocket_loader ||
$is_rocket_ie) ) continue;
$hashes_a[] = "'sha256-" . base64_encode( hash('sha256',
$body, true) ) . "'";
}
}
}}}
So this will make hashes just for known valid inline scripts.
--
Ticket URL: <https://core.trac.wordpress.org/ticket/39941#comment:122>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform
More information about the wp-trac
mailing list