Photo by Jeremy Bishop on Unsplash
HelmetJS and SwaggerUI: Avoiding headaches in your NodeJS app
avoid seeing double shege
As a Software Engineer, the importance of embracing Security-driven development cannot be overemphasized. If you realize how important this is, then you must have used one or more security middlewares such as HelmetJS. These tools work great until you introduce another such as Swagger UI to aid with API documentation.
I have encountered some of these issues and thought it'd be helpful putting some tips out there so you don't end up commenting out app.use(helmet())
at best ๐
Tips
Always make sure to render the Swagger UI template before setting up the HelmetJS middleware to avoid issues like this:
In most online references, this is how you set up a route to serve Swagger UI
const express = require('express'); const swaggerUi = require('swagger-ui-express'); const swaggerDocument = require('./swagger.json'); const app = express(); app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
This would cause issues in your application as there's a difference between using
app.use
and an Express application routing method such asapp.get
. The former is used for requiring middleware and would match all routes that stem from the input route (tears if the input is/
) irrespective of the HTTP verb. You'll find yourself sending POST requests to specific endpoints and getting back the HTML page of Swagger, not a good experience!If you are going to be rendering some HTML page for some reason, ensure you use a templating engine such as ejs or pug to avoid getting screamed at like so:
This happens because HelmetJS sets a couple of security headers by default, one of these headers is the Content-Security-Policy
header which has a default value of default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests
. This header aims to help mitigate Cross-Site-Scripting (XSS) attacks amongst other things and "safe" inline scripts need to have a SHA256 hash or a nonce to enable execution.
To fix this, we will configure a templating engine (ejs in this case), generate a hash using the crypto package, and then set the script-src
directive to 'self' 'nonce-{hash}'
like so:
...
const crypto = require("crypto");
app.use((req, res, next) => {
res.locals.cspNonce = crypto.randomBytes(16).toString('hex');
next();
});
app.use(
helmet.contentSecurityPolicy({
directives: {
scriptSrc: [(req, res) => `'nonce-${res.locals.cspNonce}'`],
},
})
);
After setting the nonce, open up your ejs file and add set the nonce value on the script tag like so:
<script nonce="<%= cspNonce %>">
const formElem = document.querySelector('#formElem');
formElem.onsubmit = async (e) => {
e.preventDefault();
let url = window.location.href;
...
NB: Do ensure the variable names match, also, If you're going to be including external scripts such as Axios via CDN, ensure you add the nonce to the script tag as well
Thanks for reading and I hope it does help you out, would update this as I encounter more issues. See ya at the next one! ๐