HelmetJS and SwaggerUI: Avoiding headaches in your NodeJS app

avoid seeing double shege

ยท

3 min read

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

  1. Always make sure to render the Swagger UI template before setting up the HelmetJS middleware to avoid issues like this:

    Bunch of errors

  2. 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 as app.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!

  3. 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:

Content-Security-Policy error

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! ๐Ÿ’–

Further reading

  1. Swagger UI documentation

  2. HelmetJS documentation

ย