[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