odan/twig-assets

CSP nonces for assets

Closed this issue · 10 comments

Hi, I'm having problems with making twig-assets play with CSP (content security policy) headers. It seems, that doing something like

{{ assets({files: [
  '@public/assets/node_modules/react/umd/react.production.min.js',
  '@public/assets/node_modules/react-dom/umd/react-dom.production.min.js',
  '@public/assets/node_modules/react-jsonschema-form/dist/react-jsonschema-form.js',
  '@public/assets/node_modules/@babel/standalone/babel.min.js',
], name: 'rjsf.js'}) }}

will not work with, i.e. https://github.com/middlewares/csp

use Middlewares\Csp;
use ParagonIE\CSPBuilder\CSPBuilder;
// some other middleware code here...
$csp = new CSPBuilder($settings['headers']['csp']);
$nonce['script_src'] = $csp->nonce('script-src');
$app->add(new Middlewares\Csp($csp));

and fail with something like

rjsf.b931541e8784057844bd090891f00c90a5df3833.js:19 Refused to execute inline script because it violates the following Content Security Policy directive: "script-src https://10.146.149.29 http://10.146.149.29 10.146.149.29 'nonce-HKva7wv9fBswKZtpuJi35815' 'unsafe-inline' 'unsafe-eval'". Note that 'unsafe-inline' is ignored if either a hash or nonce value is present in the source list.

My guess is, that since there's no easy way to add csp nonces or hashes to the resulting rjsf.js, and the rjsf.js has inline <script>, the CSP just kills the script. If my reasoning is correct, then twig-assets would need a way how to optionally add

  1. either a nonce parameter, that could be passed to it via a twig global
{{ assets( { files: [ ...], name: 'rjsf.js', nonce: cspnonce } ) }}
  1. add a twig function which would calculate hashes of the content enveloped by the function (and with ob turned on) modify response headers as done by i.e. https://github.com/nelmio/NelmioSecurityBundle#message-digest-for-inline-script-handling , so i.e.
{% cspscript %}
<script>
    window.api_key = '{{ api_key }}';
</script>
{% endcspscript %}

If I overlooked something and there's an easy way out of this problem, would you kindly point it out for me?

odan commented

It looks like an missing feature to me. It should be possible to add nonce attributes to the output, e.g. <script nonce="EDNnf03nceIOfn39fn3e9h3sdfa">…</script>

either a nonce parameter, that could be passed to it via a twig global

Ths looks good. But how do you get the nonce from the middleware? Are you planning to generate your own (fix) nonce and pass it to the CSPBuilder?

https://github.com/paragonie/csp-builder/blob/ab3f33f7fe44dbd182d25707eae8f0c3f6e803ed/src/CSPBuilder.php#L417

If I get it right, the nonce is fix per site, or is the nonce dynamic per script?

Cool! Let me explain:

So basically, a new nonce is generated for every single request. The browsers then look on the response headers of the server (generated by line https://github.com/vaizard/glued-skeleton/blob/138e15301b53f996e6ea374aa20c2bfcb1e80063/glued/middleware.php#L90). If there's a header Content-Security-Policy: script-src 'self' 'nonce-SOMERANDOMKEY', then the browser will ignore any <script></script> that doesn't have the nonce. So only <script nonce="SOMERANDOMKEY"></script> will get evaluated.

With inline scripts, this works well. As for the cached version, this I see now is a bit of a problem, because you get a new nonce for every request, so there would have to be some additional str_replace on the cached assets, if I'm not mistaken. So maybe using the digests would be more relevant. A nice explanation extracted from https://www.netsparker.com/blog/web-security/content-security-policy/ here:

Content Security Policy can also be configured to only load resources if they match defined hashes. That way it's not possible to execute resources that have been tampered with. To set such a hash the following CSP header can be used:

Content-Security-Policy: script-src 'self' 'sha256-78iKLlw3hSqddlf6qm/PGs1MvBzpvIEWioaoNxXIZwk='

This contains the hash of the following script block

<script>alert("allowed");</script>

The developer creates the hash and implement the CSP rule with the following PHP code:

header("Content-Security-Policy: script-src 'self' 'sha256-".base64_encode(hash('sha256', 'alert("allowed");', true))."'");

If I understand correctly, one would need just the list of hashes, that one could be generated when an asset gets cached and just stored somewhere as a file.

Honestly I see I'm not that well oriented in CSP as many questions pop up :) maybe the nonce would do it with the resulting rjsf.js cached asset - if the nonce gets 'inherited' by all the <script> occurrences in the rjsf.js. But didnt try that

odan commented

MDN/CSP/style-src notes the following:

'nonce-{base64-value}' A whitelist for specific inline scripts using a cryptographic nonce (number used once). The server must generate a unique nonce value each time it transmits a policy. It is critical to provide an unguessable nonce, as bypassing a resource’s policy is otherwise trivial. See unsafe inline script for example.

Because the nonce must be generated to be unique and random for every request, this is not something that we can do at build / cache time. Using a "fix" nonce is insecure because it never changes, so it could be trivially bypassed by an attacker.

To communicate the nonce value to JS and CSS we could add a nonce parameter to the assets function. This would output the nonce html attribute with the given value. It must be possible to prevent the caching of the nonce, because the nonce must be changed per request.

I think we could start with this concept and see if it works. What do you think?

Yep this makes totally sense.

For feature reference, to anybody out there fighting with CSP based XSS protection: Dist builds of some js libraries contain inline <script> tags and eval calls. Both can cause problems. For example,

These are (probably) bugs. Sometimes, some (barely) acceptable workarounds can be used - see webpack/webpack#5627 - before a proper fix is done. Another solution is to have twig-assets take care of distributing the nonces, but I think that you will be forced to used inline:true alongside with the nonces. So the good thing is, you can sacrifice performance (load time) for security. The bad thing is, you sacrifice performance.

odan commented

You can test the csp-nonce branch with support for the nonce option. The html tags (script, style) are now excluded from the cache, because the nonce must be changes on every request.

Installation:

composer require odan/twig-assets:dev-csp-nonce

Usage:

{{ assets( { files: [ ...], name: 'file.js', nonce: mycspnonce } ) }}

The result:

<script nonce="EDNnf03nceIOfn39fn3e9h3sdfa">…</script>

tested, works perfectly!

odan commented

Ok great. I'll release it soon.