Yet another SWE Blog.

How to CSP

Nigel Schuster
Nigel Schuster

I was recently looking at a friend's project. One peculiar thing that piqued my interest was the CSP policy they had configured. The relevant section of their webpack.config.js was:

const ONLOAD_NONCE = `${Math.random()}`.substring(2, 8);
...
new CspHtmlWebpackPlugin({
  'default-src': "'self'",
  'script-src': `'self' 'nonce-${ONLOAD_NONCE}'`,
}),

and their index.html was:

<head>
  <script nonce="<%= htmlWebpackPlugin.options.onloadNonce %>">
    console.log("Hello World");
  </script>
</head>

The reasoning is clear: They added the inline script and Chrome started complaining about it violating the CSP policy. The easiest way to solve this is to add a nonce. Unfortunately this solution effectively disables any protection that CSP offers. Crucially, the spec states:

The server must generate a unique nonce value each time it transmits a policy.

By including the nonce as part of the build process, it will be the same for all clients using assets with that build version.

How can an attacker leverage this?

Remember that CSP is another layer of defence. It by itself not working will not make a website vulnerable. Therefore we have to assume that there are other flaws in the website. Here is an example of how an XSS attack could happen:

At some point you decided to add a rich text field to your application, so that users can post well formatted messages. Since you are using react, you can simply use <div dangerouslySetInnerHTML={post()} /> to display the data. Unfortunately an attacker discovered that, that they crafted a post content that can behave maliciously:

<script>console.log("I can access your private data")</script>

Any user viewing the post is now at risk of having their browser data, such as cookies, stolen. Thanks to your CSP policy this inline code will not execute. However, with a nonce per build the attacker can craft the post content to be:

<script nonce="nonce-from-last-build">console.log("I can access your private data")</script>

This script will happily be executed by the browser.

What should they have done instead?

There are two approaches to mitigate this issue. Whenever you can, you should favor the hash based approach, but in some cases that may not be possible.

Hash

Instead of using a nonce for the script, it is sufficient to list the sha hash of the script in the CSP policy:

new CspHtmlWebpackPlugin({
  'default-src': "'self'",
  'script-src': `'self' 'sha256-${HASH_OF_SCRIPT}'`,
}),

In fact the plugin will do that automatically if the script is part of the build graph, so that you don't even need to list the hash.

In this approach we allowlist scripts with certain hashes. Since it is close to impossible to craft a script that matches the hash of an existing hash, this is sufficient to validate that a script is meant to be run.

Per request nonce

If the hash based approach does not suffice, you will need to generate a new nonce per request. This nonce should be inserted into the html sent to the client (potentially using a templating engine like ejs). Assuming you use webpack, you should have something similar to the following block in the html file:

<script nonce="your-nonce">
__webpack_nonce__ = 'your-nonce';
</script>

This will allow webpack to leverage the generated nonce as needed.


More Stories