Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security] Nonce reuse #82

Open
5 of 9 tasks
lweichselbaum opened this issue Jan 27, 2021 · 12 comments
Open
5 of 9 tasks

[Security] Nonce reuse #82

lweichselbaum opened this issue Jan 27, 2021 · 12 comments

Comments

@lweichselbaum
Copy link

lweichselbaum commented Jan 27, 2021

Nonce reuse

I have a question in regard to nonceEnabled:
I assume that the csp-html-webpack-plugin is only invoked at build time and not for every http request. If this assumption is correct, how can one prevent attackers from just copying CSP nonces and by that bypassing the entire CSP?

Relevant section in the CSP spec is here: https://w3c.github.io/webappsec-csp/#security-nonces

What type of issue is this? (place an x in one of the [ ])

  • bug
  • enhancement (feature request)
  • question
  • documentation related
  • testing related
  • discussion

Requirements (place an x in each of the [ ])

  • I've read and understood the Contributing guidelines and have done my best effort to follow them.
  • I've read and agree to the Code of Conduct.
  • I've searched for any related issues and avoided creating a duplicate issue.

@Nantris
Copy link

Nantris commented Feb 3, 2021

It would be great if an nonce could be passed to the page at runtime somehow.

@Nantris
Copy link

Nantris commented Feb 3, 2021

I came across this option but I'm not sure the CSP HTML Webpack Plugin makes use of it?

https://webpack.js.org/guides/csp/

I've tried setting __webpack_nonce__ but it doesn't seem to end up applied.

@AnujRNair
Copy link
Contributor

You're correct in your assumption - nonces are only useful if they are changing often, ideally on every page load. If your app will be deployed for long periods of time without its source code changing, you should either:

  1. Use shas instead, which checks the contents of a script block matches in a more strict fashion
  2. Generate your own nonces server side and put them into your html source on each page load, using the __webpack_nonce__ feature as suggested above. Here's a stackoverflow post with some more details

At Slack, we have opted to only use shas

@Nantris
Copy link

Nantris commented Feb 10, 2021

@AnujRNair thanks for your reply! Are there any potential risks that nonce can address which hashes cannot?

We tried getting __webpack_nonce__ working without luck, so if hashes alone eliminate any need for an nonce, that would be great news.

@AnujRNair
Copy link
Contributor

Not to my knowledge. As far as I am aware, they're as good as each other. Someone can correct me if you find otherwise!

  • Hashes are useful for when you don't want to serve your HTML through a server request first. The hashes are precomputed and put directly into the HTML. Nothing more to do
  • Nonces are useful when you have dynamic JS being generated by the server, which is being added to the HTML. It's easier to create a nonce then parse the file and generate a hash

@lweichselbaum
Copy link
Author

Thanks for confirming @AnujRNair!
I think that CSP can serve as strong defence-in-depth mechanism, if configured properly. Unfortunately, it's very easy to misconfigure a CSP and by that making it trivially bypassable which will not only take away any security benefits, but will also give a false sense of security.

I really like this module, but I think the way (esp. the defaults) are implemented right now could cause many applications to end up with a trivially bypassable CSP. I think it's crucial to provide a secure-by-default configuration as most users won't know all the intricate details about CSP:

  • nonce reuse: if nonces are not changing on every page reload (even cached responses are a problem!) they'll cause the entire CSP to become bypassable (this is similar to XSRF tokens). Nonces changing often (even if every 10 minutes) are still easy to bypass as the attacker could fetch the current nonce in less than a second.

    • This module enables nonces by default, so even if your hashing everything, the presence of a re-used nonce will make the entire CSP bypassable as the policy allows executing of scripts using a hash or a nonce.
    • It's not trivial to setup SPAs with regenerated nonces for every page load. Unless this module can provide this integration, it might be better to disable nonces by default and warn developers that if they enable noncing they'd need to make sure that they're regenerated on every page load themselves.
  • host allowlists: host entries in the CSP often lead to bypasses (JSONP, hosting of AngularJS, ..), esp. if there are CDNs in the allowlist (which is often necessary). More about how easy it is to bypass allowlists in CSP (even automatically) can be found here.

    • This module could add 'strict-dynamic' by default, which would cause the entire allowlist to be ignored and therefore would remove all potential allowlist based bypasses.

    • However, when using hashes alone (which likely is the recommendation of SPAs if they can't generated nonces on every page load) it's not possible to hash sourced scripts (actually it is, but only in Chrome right now..). Since alllowlists shouldn't be used either the only way to get a hash-only policy working is to rewrite sourced scripts that can't be inlined (for example if they're hosted somewhere else) into a hashable inline script block that dynamically loads these scripts.
      E.g. these two sourced scripts need to be converted into a hashable inline script.:

      <script src="https://example.org/foo.js"></script>
      <script src="https://example.org/bar.js"></script>
      <!-- 'sha256-XuHWTNmJH6UK7Qi9Te9PhtU5CRMuihd33ChamIrBDsI=' -->
      <script>
      var scripts = [ 'https://example.org/foo.js', 'https://example.org/bar.js'];
      scripts.forEach(function(scriptUrl) {
        var s = document.createElement('script');
        s.src = scriptUrl;
        s.async = false; // to preserve execution order
        document.head.appendChild(s);
      });
      </script>

Regarding @slapbox's question on nonces vs. hashes:

  • hashes are slightly more resilient as they don't suffer from nonce exfiltration issues. But in practice hashes are harder to use as it's not possible to hash sourced scripts (browser support and if fetched scripts are not static).

@Nantris
Copy link

Nantris commented Feb 11, 2021

Great writeup @lweichselbaum.

I think it's crucial to provide a secure-by-default configuration as most users won't know all the intricate details about CSP

I think this part is a bit tough though. Any secure CSP will break most projects and will only be fixable by users who understand CSP in the first place. Providing a secure example isn't enough and so I feel like directing people to external resources might be best here.

@lweichselbaum
Copy link
Author

Thank you!

Any secure CSP will break most projects and will only be fixable by users who understand CSP in the first place.

I think it's possible to have a secure CSP without breaking most projects.
The rewriting of sourced (external) scripts (which were previously allowed by host allowlists or nonces) into a hashable inline script as shown in the code snippet above can be automated. With that a CSP based only on hashes (without allowlists) can be used as a secure default.
Of course, if you can use nonces that change on every page load (nonce actually stands for number used once), it would be easier to deploy a strong CSP as you wouldn't have to rewrite script tags.

@Nantris
Copy link

Nantris commented Mar 3, 2021

@AnujRNair I found that hashes aren't applied except to inlined scripts and styles - so I'm wondering if you have any advice for getting __webpack_nonce__ working, since otherwise we're back to the nonce reuse issue.

@Nantris
Copy link

Nantris commented Mar 3, 2021

@AnujRNair how are you going about using SHAs at Slack? It seems like this is the route we'll have to go, but it's not really supported yet in the plugin. Any advice for how to move forward? It seems like we've just hit an unbreakable wall here.

@AnujRNair
Copy link
Contributor

Sorry for the delay in getting back to you here

@lweichselbaum thanks so much for the detailed writeup and presentation - it was extremely useful.
I would love to create a secure-out-of-the-box plugin here - it looks like we probably can't use nonces since webpack generates everything statically at compile time, so we might have to switch to using hashes only and deprecate nonces to ensure we don't provide a false sense of security. Unless you know of a way that we might be able to use nonces with webpack here?

@slapbox that is correct - only inline styles and scripts are hashed at the moment. #87 is exploring whether we can hash external script and style tags as well

At Slack, we only use hashes, and we generate our script tags as @lweichselbaum recommended. We do this by defining a custom template, and passing it to the HtmlWebpackPlugin instance.

You can do the same with something similar:

webpack config:

function generateTemplateParams(compilation, assets) {
  return {
    assetsJs: assets.js,
    assetsCss: assets.css,
  }
}

new HtmlWebpackPlugin({
  templateParameters: generateTemplateParams,
  template: 'path/to/template.ejs',
  inject: false, // don't inject script and style tags - we'll do so manually

template.ejs:

<% _.each(assetsJs, function(js, idx) { %>
	<script>
		const s<%= idx %> = document.createElement('script');
		s<%= idx %>.src = '<%= js %>';
		document.getElementsByTagName('head')[0].appendChild(s<%= idx %>);
	</script>
<% }) %>

This is the basics, but there's lots more customization you can do there

@eamodio
Copy link

eamodio commented Jul 9, 2021

Maybe there could be a way to have the nonce injection be a replaceable token, so that the nonce token could be replaced dynamically at runtime? I would like that ability, because I am attempting to do just that, until a hash can be generated for an external script/stylesheet.

I've got this working currently, by overriding the createNonce method and replacing it with a token.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants