CSP nonce with Node.js and EJS

Photo by Jaye Haych on Unsplash

CSP nonce with Node.js and EJS

Using a nonce for CSP with Node.js and EJS

ยท

11 min read

Intro

In this post, I will not dive into CSP's details. The previous link is enough for a simple introduction to the subject, but if you wish to go deeper I'll suggest taking a look at :

CSP: script-src

CSP is mainly a way to declare allowed resources to load on a domain or a particular route, to reduce the risk of Cross-site scripting (XSS) attacks. When a script loads into a webpage, the browser blocks the script if it's not defined in the script-src directive of the CSP as an allowed resource. When used, CSP will also block inline script tags like :

<script>
    doSomething()
</script>

as well as inline event handlers like :

<button id="btn" onclick="doSomething()"></button>

CSP: style-src

Like script-src, style-src is used to declare the valid sources of styles.

CSP style-src directive will block inline style tags and inline style attributes. So, the following will not load :

<!-- Inline style tag gets ignored -->
<style>
    #my-div {
        background-color: red;
    }
</style>

<!-- Inline style attribute gets also ignored -->
<div id="my-div" style="background-color:red">I will not have a red background !</div>

Note that the style-src directive will also block styles applied in JS via setAttribute. The following example will not be rendered :

document.getElementById("my-div").setAttribute("style", "background-color:red;")

However, styles set on the element's style property will work. The following example will be rendered :

document.getElementById("my-div").style.backgroundColor = "red"

Unsafe expressions

There are unsafe ways to whitelist inline script tags, inline event handlers, inline style tags and inline styles, but I'm not going to talk about them because they are unsafe and break the whole point of a CSP!

Setting CSP in Node.js

To define allowed resources in a CSP via Node.js, we have to declare them as a response header :

  1. The user makes a request

  2. The server sends a response

  3. The browser loads the page along with the allowed resources

It's in the response header that a CSP lives and where the browser will look to know what he can render.

Using Express, we can simply do the following :

const express = require("express")
const app = express()

// Set CSP as a middleware function
app.use(function (req, res, next) {
    res.setHeader(
        "Content-Security-Policy",
        "default-src 'self'; script-src 'self'; style-src 'self'; font-src 'self'; img-src 'self'; frame-src 'self'"
    )

    next()
})

app.get("/", (req, res) => {
    res.send("Hello World!")
})

app.listen(3000, () => {
    console.log(`App ๐Ÿš€ @ http://localhost:3000`)
})

As you can see, we have defined the most used directives to 'self', meaning that we are only allowing resources from the current host (including URL scheme and port number).
If you run this app (node index), and follow the link, you'll get a nice Hello World! If you open the Console (F12), you'll see nothing since we didn't do much for now.

EJS

To render an HTML page, load external scripts and styles to test our CSP, I'll be using EJS.
Feel free to use any other template engine that suits your needs. I highly recommend EJS for the following reason :

EJS is a simple templating language that lets you generate HTML markup with plain JavaScript.

After installing EJS (npm i ejs), we'll have to create a views folder, at the root of the app, to store the .ejs files. EJS will look inside this folder to render your page(s) the way you instruct him to do. In this folder, create a file called index.ejs (/views/index.ejs) with the following content :

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>
    <body>
        <h1>Hello from EJS !</h1>
    </body>
</html>

Yes, a .ejs file is an HTML file in which we can use plain JavaScript, we'll see that in a moment.

Update our main server file, index.js, to look like this :

const express = require("express")
const app = express()

// Set CSP as a middleware function
app.use(function (req, res, next) {
    res.setHeader(
        "Content-Security-Policy",
        "default-src 'self'; script-src 'self'; style-src 'self'; font-src 'self'; img-src 'self'; frame-src 'self'"
    )

    next()
})

// Set EJS as a template engine
app.set("view engine", "ejs")

// Use EJS to render our page(s)
app.get("/", (req, res) => {
    res.render("index") // renders index.ejs
})

app.listen(3000, () => {
    console.log(`App ๐Ÿš€ @ http://localhost:3000`)
})

External resources

Now, to test our CSP, we just have to load some external resources.
Let's bring on Pure.css and Lodash. Update index.ejs to look like this :

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
        <!-- Pure.css -->
        <link
            rel="stylesheet"
            href="https://unpkg.com/purecss@2.1.0/build/pure-min.css"
            integrity="sha384-yHIFVG6ClnONEA5yB5DJXfW2/KC173DIQrYoZMEtBvGzmf0PKiGyNEqe9N6BNDBH"
            crossorigin="anonymous"
        />
    </head>
    <body>
        <h1>Hello from EJS !</h1>

        <!-- Lodash -->
        <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
    </body>
</html>

Save index.ejs, reload the app in the browser, and open the Console :

โš ๏ธ Loading failed for the <script> with source โ€œhttps://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.jsโ€.
๐Ÿ›‘ Content Security Policy: The pageโ€™s settings blocked the loading of a resource at https://unpkg.com/purecss@2.1.0/build/pure-min.css (โ€œstyle-srcโ€).
๐Ÿ›‘ Content Security Policy: The pageโ€™s settings blocked the loading of a resource at https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js (โ€œscript-srcโ€).

The previous Console is Firefox, the next one is Chrome :

๐Ÿ›‘ Refused to load the stylesheet 'https://unpkg.com/purecss@2.1.0/build/pure-min.css' because it violates the following Content Security Policy directive: "style-src 'self'". Note that 'style-src-elem' was not explicitly set, so 'style-src' is used as a fallback.
๐Ÿ›‘ Refused to load the script 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js' because it violates the following Content Security Policy directive: "script-src 'self'". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.

Now, you can see that our CSP has blocked Pure.css and Lodash, so everything is working as expected since they are not defined in our CSP as some allowed resources to load in the browser.

Helmet

Imagine, not necessarily because it happens when you are creating an app, having a reasonable amount of scripts and styles to whitelist.
The CSP middleware function in the main server file will grow and become sort of ugly and hard to maintain.
An excellent alternative would be to use Helmet if you're using Express.

Helmet helps you secure your Express apps by setting various HTTP headers.

Let's add Helmet to our Express app with the following command npm i helmet.
To easily maintain our CSP, let's move it inside a middleware folder, at the root of the app, in a file called helmet.js. The app structure looks like the following tree :

Application's root without node_modules folder
โ”œโ”€โ”€ index.js
โ”œโ”€โ”€ middleware
โ”‚  โ””โ”€โ”€ helmet.js
โ”œโ”€โ”€ package-lock.json
โ”œโ”€โ”€ package.json
โ””โ”€โ”€ views
  โ””โ”€โ”€ index.ejs

Let's add a CSP with Helmet (/middleware/helmet.js) :

const helmet = require("helmet")

module.exports = helmet()

and update index.js to call this middleware :

const express = require("express")
const app = express()

// Set CSP using Helmet
const helmet = require("./middleware/helmet")
app.use(helmet)

// Set EJS as a template engine
app.set("view engine", "ejs")

// Use EJS to render our page(s)
app.get("/", (req, res) => {
    res.render("index") // renders index.ejs
})

app.listen(3000, () => {
    console.log(`App ๐Ÿš€ @ http://localhost:3000`)
})

Save both files, refresh your browser, and open the Console :

โš ๏ธ Content Security Policy: Couldnโ€™t process unknown directive โ€˜script-src-attrโ€™
โš ๏ธ Loading failed for the <script> with source โ€œhttps://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.jsโ€.
๐Ÿ›‘ Content Security Policy: The pageโ€™s settings blocked the loading of a resource at https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js (โ€œscript-srcโ€).

The previous Console is Firefox, the next one is Chrome :

๐Ÿ›‘ Refused to load the script 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js' because it violates the following Content Security Policy directive: "script-src 'self'". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.

As you can see, now only Lodash is blocked ๐Ÿค” And Firefox is throwing a warning for an unknown directive.

Under the hood, a lot is happening, and it will take a series of posts to explain in detail each header and how to configure them...

But just that you know, Helmet sets a bunch of default values to protect your endpoint.
One of them is :
style-src 'self' https: 'unsafe-inline';
This is the directive allowing Pure.css.
It means : "allow any styles' source from my domain, or styles' source served over https, or inline styles".
But as I've said before, any 'unsafe-...' expression is unsafe and should not be used unless there is no other option...
I've linked at the beginning of this section to Helmet's documentation.
We'll be addressing all issues, properly, in the next and last section.

Hash and Nonce

To allow the execution of inline scripts, inline event handlers and inline styles, a hash or a nonce that matches the inline code can be specified, to avoid using the 'unsafe-inline' expression.

Hash


A hash is a string composed of two parts connected by a dash with each other :

  1. The cryptographic algorithm that is used to create the hash value.

  2. The base64-encoded hash of a script or style.

CSP supports sha256, sha384 and sha512.

But when you hash a script or a style, the generated string matches only the hashed code, meaning that if the code changes in any way (dot, space, new line, comment, added/removed/formatted code), the hash will no longer match the code who gets blocked! In this case, you'll have to regenerate a hash that matches the modified code...
It's a time-consuming process if your code changes a lot, but commonly used and recommended over a nonce, especially for static scripts.

From MDN :

Note: Only use nonce for cases where you have no way around using unsafe inline script or style contents. If you don't need nonce, don't use it. If your script is static, you could also use a CSP hash instead. (See usage notes on unsafe inline script.) Always try to take full advantage of CSP protections and avoid nonces or unsafe inline scripts whenever possible.

Nonce


On the other hand, a nonce is a cryptographic number used once, generated using a cryptographically secure random number generator, that must be unique for each HTTP response as a random base64-encoded string of at least 128 bits of data.

So, in the case of server-side rendering, a nonce is more often used and can be used for inline and external scripts and styles.
Note that a nonce-value will not allow stylesheet requests originating from the @import rule!

To use a nonce, for a script, we have to declare at the top of our script-src directive the 'strict-dynamic' expression to allow the execution of that script as well as any script loaded by this root script.
When using the 'strict-dynamic' expression, other expressions such as 'self' or 'unsafe-inline' will be ignored.

I like to keep my code clean and maintainable because at one point or another, I'll want to update it, this is why I split (as most developers) my code into pieces where each one is easily trackable in a near or far future. Let's add a file called nonces.js in the middleware folder, the app structure now looks like the following tree :

Application's root without node_modules folder
โ”œโ”€โ”€ index.js
โ”œโ”€โ”€ middleware
โ”‚  โ”œโ”€โ”€ helmet.js
โ”‚  โ””โ”€โ”€ nonces.js
โ”œโ”€โ”€ package-lock.json
โ”œโ”€โ”€ package.json
โ””โ”€โ”€ views
  โ””โ”€โ”€ index.ejs

Open nonces.js and add the following content :

// Determining if crypto support is unavailable
let crypto
try {
    crypto = require("crypto")
} catch (err) {
    console.log("crypto support is disabled!")
}

// Generating a nonce for Lodash with crypto
let lodashNonce = crypto.randomBytes(16).toString("hex")

// Maybe you'll have some other later
module.exports = { lodashNonce }

The crypto module is a built-in functionality of Node.js but it's better to check if it's included or not, in our installation, just like the docs.

Now, update helmet.js :

const helmet = require("helmet")
let { lodashNonce } = require("./nonces")

module.exports = helmet({
    contentSecurityPolicy: {
        directives: {
            scriptSrc: [
                "'strict-dynamic'", // For nonces to work
                `'nonce-${lodashNonce}'`,
            ],
            scriptSrcAttr: null, // Remove Firefox warning
            styleSrc: ["'self'", "https:"], // Remove 'unsafe-inline'
        },
    },
})

This way is much more elegant, clean and maintainable than a middleware function in the main server file.

Finally, we'll have to pass the generated nonce from the route where we need to load the script as a variable and grab this variable in the route's template where the script tag is.
I'll be commenting on the code to explain the steps, the first file is /index.js, and the second one is /views/index.ejs :

const express = require("express")
const app = express()

// Set CSP with helmet
const helmet = require("./middleware/helmet")
app.use(helmet)

app.set("view engine", "ejs")

/**
 * 1- We require lodashNonce
 * 2- This is our route "/"
 * 3- We are rendering "index.ejs"
 * 4- We pass lodashNonce into the route,
 * with the second argument of res.render
 * which is an object, as a variable
 * 5- This object is now accessible
 * in the EJS template file
 * 6- We'll get lodashNonce value
 * by the ourGenerateNonce key
 * in the EJS template file
 * 7- That's it here, open index.ejs below
 */
let { lodashNonce } = require("./middleware/nonces")
app.get("/", (req, res) => {
    res.render("index", { ourGenerateNonce: lodashNonce })
})

app.listen(3000, () => {
    console.log(`App ๐Ÿš€ @ http://localhost:3000`)
})
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
        <!-- Pure.css -->
        <!-- 
            Use JSDELIVR to load Pure.css instead of UNPKG
        -->
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/purecss@2.1.0/build/pure-min.min.css" />
    </head>
    <body>
        <h1>Hello from EJS !</h1>

        <!-- Lodash -->
        <!-- 
            Set the nonce attribute to ourGenerateNonce
            using EJS output value tag <%= %>
        -->
        <script
            nonce="<%= ourGenerateNonce %>"
            src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"
        ></script>
    </body>
</html>

Save those files, reload your browser and open the browser's console ๐Ÿฅณ๐ŸŽ‰๐ŸŽŠ
Congrats, you've just loaded an external script using a nonce!

Hope that this post was helpful.

SYA,
LebCit.

ย