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
- either a nonce parameter, that could be passed to it via a twig global
{{ assets( { files: [ ...], name: 'rjsf.js', nonce: cspnonce } ) }}
- 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?
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?
If I get it right, the nonce is fix per site, or is the nonce dynamic per script?
Cool! Let me explain:
- I'm adding the csp middleware like this: https://github.com/vaizard/glued-skeleton/blob/138e15301b53f996e6ea374aa20c2bfcb1e80063/glued/middleware.php#L88
- On line 89, I let the CSPbuilder generate the nonce with
$csp->nonce('script-src')
(now commented out) - I pass the nonce to another middleware (line 93), which just adds makes it a twig global - see https://github.com/vaizard/glued-skeleton/blob/138e15301b53f996e6ea374aa20c2bfcb1e80063/glued/Core/Middleware/TwigCspMiddleware.php
- In the twigs, I just add the csp_nonce variable to inline scripts https://github.com/vaizard/glued-skeleton/blob/138e15301b53f996e6ea374aa20c2bfcb1e80063/glued/Core/Views/templates/partials/rjsf.twig#L10 (similarly, one would do this for other inline assets such as inline css).
- The missing feature would be the ability to add the csp_nonce variable here https://github.com/vaizard/glued-skeleton/blob/138e15301b53f996e6ea374aa20c2bfcb1e80063/glued/Core/Views/templates/partials/rjsf.twig#L8
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
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,
yarn add react-jsonschema-form
will give you exactly this https://unpkg.com/react-jsonschema-form@2.0.0-alpha.1/dist/react-jsonschema-form.js (which contains<script>document.F=Object<\/script>
)- similarly babel-standalone (https://unpkg.com/babel-standalone@6.26.0/babel.min.js) contains evals.
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.
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!
Ok great. I'll release it soon.