This post is about using a nonce for CSP with Node.js and EJS.
Intro
In this post, I will not dive into CSP's details. The link in the description, above, is enough for a simple introduction to the subject, but if you wish to go deeper I'll suggest to take a look at:
CSP: script-src
CSP is manly 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 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 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:
- The user makes a request
- The server sends a response
- The browser loads the page along with 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) only.
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.
Fell 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
with the following content:
/views/index.ejs
<!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, an .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 to look like this:
/index.js
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:
/views/index.ejs
<!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:
Firefox 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β).
Chrome Console
π 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 have blocked Pure.css and Lodash, so everything is working as expected since they are not defined in our CSP as 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, a 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:
/index.js
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:
Firefox 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β).
Chrome Console
π 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 their is really 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:
- The cryptographic algorithm used to create the hash value.
- 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
:
/middleware/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 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 were 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 the code to explain the steps:
/index.js
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`)
})
/views/index.ejs
<!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.
Next one will be about EJS.