Is setting a CSP nonce supposed to work magically in node express?

656 views Asked by At

When I try this, as part of my helmet.js setup I get errors.

'script-src': ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`],

It worked serving it over http://127.0.0.1:3000/ but on the https://www.example.com/ production server, in Firefox, I get:

 Content Security Policy: The page's settings blocked the loading of a resource at inline ("script-src").

In Chrome it is more informative:

Refused to execute inline script because it violates the following Content Security Policy directive:
  "script-src 'self' 'nonce-b6c07d77ebd0956f28936386c1abef1b'".
  Either the 'unsafe-inline' keyword, a hash ('sha256-YiQnvxxxnwHItf/iksvxxxxKGGMABdQbOU='), or a nonce ('nonce-...') is required to enable inline execution.

The nonce value changes each time I reload, which matches my understanding. I assumed that helmetjs would automatically be inserting this nonce into headers of the js files, if it was needed? I've followed the configuration shown at https://helmetjs.github.io/#reference and haven't spotted any other configuration I have to do.

If I add "'strict-dynamic'", to the list, both browsers still complain, but they complain about specific files now: e.g. https://www.example.com/js/mylib.js (even though it is same domain as the loaded page) We don't load JS from 3rd party servers, it is always served by this server. (But we do have a sibling domain, https://auth.example.com/ running a frame.)

If I change to this then everything works:

'script-src': ["'self'", "'unsafe-inline'"],

If I went down the route of adding sha256-YiQn... for every JS file to the CSP that will mean needing to modify production server config each time we push a new bug fix to any of the websites it serves. That is not the way. I must be missing something??

I'm also curious why it worked fine on http://127.0.0.1:3000/ (but framing and connecting to the production https://auth.example.com/ and https://db.example.com/). Is there a way I can test locally, which will be more realistic?

ADDITIONAL:

We are serving static html that has been built by webpack: an index.html and then some *.js files. If the nonce needs to be inserted, is it just the index.html that needs to change to be served dynamically? (If the *.js files too, where would you put it??)

If the index.html is static, is it better to use the sha256 hash approach instead of a nonce? Or would it still be better to serve it dynamically, inserting the nonce?


BTW, recent browser versions, so I think we can expect full CSP3 support; we have no need to support IE11 or anything older.

For reference the full non-working helmetjs configuration:

const helmet = require('helmet')
const crypto = require('crypto')

...

app.use((req, res, next) => {
  res.locals.cspNonce = crypto.randomBytes(16).toString('hex')
  next()
})
app.use(helmet({
  xFrameOptions: { action: 'deny' },
  contentSecurityPolicy: {
    directives: {
      'script-src': ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`],
      'frame-src': ["'self'", 'auth.example.com'],
      'connect-src': [
        "'self'",
        'auth.example.com',
        'db.example.com',
      ],
      'worker-src': ["'self'"]
    },
  },
}),

Helmet version 7.1.

1

There are 1 answers

4
VonC On BEST ANSWER

The CSP nonce (see "Helmet Content Security Policy middleware") should work the same in both development and production environments. The fact that it works on localhost but not on your production server suggests a difference in the environment or the way scripts are included in your HTML.

Make sure your HTML templates correctly apply the nonce to each <script> tag.
For instance, in an Express.js view engine like EJS, you should include the nonce like this:

<script nonce="<%= res.locals.cspNonce %>">
    // Your inline script here
</script>

Also, the CSP policy might behave differently under HTTPS due to stricter security standards. That could explain why it works on localhost (HTTP) but not on your production server (HTTPS). Make sure your CSP headers are properly set in both environments.

Adding 'strict-dynamic' (supported with Helmet 3.9.0+, Q4 2017) to your CSP can sometimes help, but it should be used carefully as it allows scripts to load other scripts. That might not be suitable for your security requirements.

Warning: while using 'unsafe-inline' makes it work, it defeats the purpose of a strict CSP. It is not recommended for production as it allows all inline scripts, making your site vulnerable to XSS attacks.

To test locally in a more realistic way, consider setting up HTTPS on your local server. Tools like ngrok, localtunnel, or setting up a self-signed SSL certificate can help mimic a production-like environment.

const helmet = require('helmet');
const crypto = require('crypto');
const express = require('express');
const app = express();

app.use((req, res, next) => {
    res.locals.cspNonce = crypto.randomBytes(16).toString('hex');
    next();
});

app.use(helmet({
    xFrameOptions: { action: 'deny' },
    contentSecurityPolicy: {
        directives: {
            'script-src': ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`],
            'frame-src': ["'self'", 'auth.example.com'],
            'connect-src': ["'self'", 'auth.example.com', 'db.example.com'],
            'worker-src': ["'self'"]
        },
    },
}));

// Rest of your express app setup...

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

Do properly include the nonce in your script tags as shown above.


We are serving static HTML that has been built by Webpack. So that needs to change to be served as dynamic HTML?

To implement Content Security Policy (CSP) with Helmet in your Node.js Express application, especially when using nonces for inline scripts, it is often necessary to serve dynamic content. That is because the nonce value must be unique for each request and included in the <script> tags of your HTML content.
Since you are serving static HTML built by Webpack, that would present... a challenge.

Normally, I would go with serving static HTML with dynamic nonces: that means converting your static HTML into a form where the nonce can be inserted for each request. If your HTML is generated by Webpack, you would typically need to adjust your setup to serve it dynamically for nonce insertion.
That does not mean your entire HTML needs to be generated on the fly; only the part involving nonce insertion.
For external JavaScript files linked in your HTML, you do not need to insert the nonce into the files themselves. The nonce is required only in the <script> tag that references them in your HTML.

That would mean:

  • renaming your index.html to index.ejs.
  • modifying the <script> tags to include the nonce:
    <script nonce="<%= res.locals.cspNonce %>"></script>
    
  • serving the EJS file dynamically in Express.js:
    app.get('/', (req, res) => {
       res.render('index', { nonceValue: res.locals.cspNonce });
    });
    

But, as an alternative to dynamic nonce insertion, you could use SHA256 hashes for your inline scripts. That approach is more static-friendly.
Generate hashes for each of your inline scripts. These hashes are then included in your CSP header. That method is more compatible with static HTML since it does not require dynamic content generation.
The main drawback is the need to update your CSP header whenever your inline scripts change.

If converting to dynamic HTML is not feasible or if your inline scripts rarely change, consider using SHA256 hashes.

For every inline script in your HTML, you need to generate a SHA256 hash. Make sure the hash is generated for the exact content of the script, including any whitespace and line breaks, but excluding the <script> tags themselves. You can use various tools to generate these hashes. For example, in Node.js, you could use the crypto module.

In your CSP header, add each hash using the format 'sha256-Base64EncodedHash'.
When setting up Helmet's CSP middleware, include these hashes in the scriptSrc directive.

Suppose you have an inline script like this in your HTML:

<script>
  console.log('Hello, world!');
</script>

You would first generate a SHA256 hash of console.log('Hello, world!');. Say a base64 encoded hash Xyz123.
Then, in your Express.js application, you would configure Helmet like this:

const helmet = require('helmet');

app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: [
      "'self'",
      "'sha256-Xyz123'" // The base64 encoded SHA256 hash of your inline script
    ],
    // other directives
  }
}));

But: every time you change an inline script, you must regenerate and update the corresponding SHA256 hash in your CSP header. That can become cumbersome if you frequently modify inline scripts.
And, while using hashes is secure, it is less flexible than nonces: as I mentioned before, if you have a lot of inline scripts or if they often change, consider serving your HTML dynamically to use nonces instead.


In this case it turned out to be simpler, as all but one HTML file only used <script src="..."></script> and it seems they don't need a nonce/hash.
Tracking down the one bit of inline javascript in a component was the really hard bit. It's a shame eslint doesn't point them out.

+--------------------------------------------------+
|                 Web Application                  |
+--------------------------------------------------+
               |                              |
               |                              |
+--------------v-------------+    +-----------v------------+
| External <script src="...">|    | Inline <script> in One |
| (No nonce/hash required)   |    | HTML File (Hash used)  |
+----------------------------+    +------------------------+
                                      |
                                      |
                             +--------v-------+
                             | Locate Inline  |
                             | JavaScript     |
                             +----------------+
                                      |
                                      |
                         +------------v-----------+
                         | Generate SHA256 Hash   |
                         | of Inline Script       |
                         +------------------------+
                                      |
                                      |
             +------------------------v------------------------+
             | Include SHA256 Hash in CSP Header for inline    |
             | scripts                                         |
             +-------------------------------------------------+

Indeed, <script src="..."></script> tags that reference external JavaScript files do not require a nonce or a hash in the CSP, as long as the sources are explicitly allowed in the CSP directives.

For the inline JavaScript that you found, using a hash (as you have done) is a good solution if it does not change often.

For future reference, and to address the difficulty you experienced in locating inline scripts, you might consider

  • Code Search Tools: Utilize code search tools or IDE features that can search across your entire codebase. Regular expressions can be particularly useful for this. For instance, searching for <script> tags that do not include src attributes could help you locate inline scripts.

  • Linter Configurations: While ESLint out-of-the-box might not flag inline scripts in HTML, you can configure ESLint with specific rules or plugins that focus on CSP or security best practices.
    You can also consider using additional linting tools that are more HTML-centric or specifically designed to detect potential CSP violations.

    For instance:

    {
    "extends": "eslint:recommended",
    "rules": {
        "no-eval": "error", // Disallow the use of eval()
        "no-inline-comments": "off", // Turn off inline comments if you want
        "no-script-url": "error" // Disallow javascript: urls
    },
    "env": {
        "browser": true,
        "es6": true,
        "node": true
    }
    }
    
  • Automated CSP Tools: There are tools available that can help generate CSP headers based on your website's actual resource usage. These tools can sometimes identify inline scripts as part of their analysis.
    (CSP Evaluator, Mozilla Observatory, pending the 2.0, Report URI CSP Generator, Security Headers, ...)