Security in Node.js Applications: Using Helmet for across-the-board security in Express applications

While it was useful to implement HTTPS, that’s not the end of implementing security measures. It’s hardly the beginning of security, for that matter. The browser makers working with the standards organizations have defined several mechanisms for telling the browser what security measures to take. In this section, we will go over some of those mechanisms, and how to implement them using Helmet.

Helmet (https://www.npmjs.com/package/helmet) is, as the development team says, not a security silver bullet (do Helmet’s authors think we’re trying to protect against vampires?). Instead, it is a toolkit for setting various security headers and taking other protective measures in Node.js applications. It integrates with several packages that can be either used independently or through Helmet.

Using Helmet is largely a matter of importing the library into node_modules, making a few configuration settings, and integrating it with Express.

In the notes directory, install the package like so:

$ npm install helmet –save 

Then add this to notes/app.mjs:

import helmet from ‘helmet’;…

const app = express();

export default app;

app.use(helmet());

That’s enough for most applications. Using Helmet out of the box provides a reasonable set of default security options. We could be done with this section right now, except that it’s useful to examine closely what Helmet does, and its options.

Helmet is actually a cluster of 12 modules for applying several security techniques. Each can be individually enabled or disabled, and many have configuration settings to make. One option is instead of using that last line, to initialize and configure the sub-modules individually. That’s what we’ll do in the following sections.

1. Using Helmet to set the Content-Security- Policy header

The Content-Security-Policy (CSP) header can help to protect against injected malicious JavaScript and other file types.

We would be remiss to not point out a glaring problem with services such as the Notes application. Our users could enter any code they like, and an improperly behaving application will simply display that code. Such applications can be a vector for JavaScript injection attacks among other things.

To try this out, edit a note and enter something like this:

<script src=”https://pirates.den/malicious.js”></script> 

Click the Save button, and you’ll see this code displayed as text. A dangerous version of Notes would instead insert the <script> tag in the notes view page so that the malicious JavaScript would be loaded and cause a problem for our visitors.

Instead, the <script> tag is encoded as safe HTML so it simply shows up as text on the screen. We didn’t do anything special for that behavior, Handlebars did that for us.

Actually, it’s a little more interesting. If we look at the Handlebars documentation, http://handlebarsjs.com/expressions.html, we learn about this distinction:

{{encodedAsHtml}}

{{{notEncodedAsHtml}}}

In Handlebars, a value appearing in a template using two curly braces ({{encoded}}) is encoded using HTML coding. For the previous example, the angle bracket is encoded as &lt; and so on for display, rendering that JavaScript code as neutral text rather than as HTML elements. If instead, you use three curly braces ({{{notEncoded}}}), the value is not encoded and is instead presented as is. The malicious JavaScript would be executed in your visitor’s browser, causing problems for your users.

We can see this problem by changing views/noteview.hbs to use raw HTML output:

{{#if note}}<div id=”notebody”>{{{ note.body }}}</div>{{/if}}

We do not recommend doing this except as an experiment to see what happens. The effect is, as we just said, to allow our users to enter HTML code and have it displayed as is. If Notes were to behave this way, any note could potentially hold malicious JavaScript snippets or other malware.

Let’s return to Helmet’s support for the Content-Security-Policy header. With this header, we instruct the web browser the scope from which it can download certain types of content. Specifically, it lets us declare which domains the browser can download JavaScript, CSS, or Font files from, and which domains the browser is allowed to connect to for services.

This header, therefore, solves the named issue, namely our users entering malicious JavaScript code. But it also handles a similar risk of a malicious actor breaking in and modifying the templates to include malicious JavaScript code. In both cases, telling the browser a specific list of allowed domains means references to JavaScript from malicious sites will be blocked. That malicious JavaScript that’s loaded from pirates.den won’t run.

To see the documentation for this Helmet module, see https://helmetjs.github.io/ docs/csp/.

There is a long list of options. For instance, you can cause the browser to report any violations back to your server, in which case you’ll need to implement a route handler for /report-violation. This snippet is sufficient for Notes:

app.use(helmet.contentSecurityPolicy({

directives: {

defaultSrc: [“‘self'”],

scriptSrc: [“‘self'”, “‘unsafe-inline'” ],

styleSrc: [“‘self'”, ‘fonts.googleapis.com’ ],

fontSrc: [“‘self'”, ‘fonts.gstatic.com’ ],

connectSrc: [ “‘self'”, ‘wss://notes.geekwisdom.net’ ]

}

}));

For better or for worse, the Notes application implements one security best practice—all CSS and JavaScript files are loaded from the same server as the application. Therefore, for the most part, we can use the ‘self’ policy. There are several exceptions:

  • scriptSrc: Defines where we are allowed to load JavaScript. We do use inline JavaScript in noteview.hbs and index.hbs, which must be allowed.
  • styleSrc, fontSrc: We’re loading CSS files from both the local server and from Google Fonts.
  • connectSrc: The WebSockets channel used by Socket.IO is declared here.

To develop this, we can open the JavaScript console or Chrome DevTools while browsing the website. Errors will show up listing any domains of failed download attempts. Simply add such domains to the configuration object.

1.1. Making the ContentSecurityPolicy configurable

Obviously, the ContentSecurityPolicy settings shown here should be configurable. If nothing else the setting for connectSrc must be, because it can cause a problem that prevents Socket.IO from working. As shown here, the connectSrc setting includes the URL wss://notes.geekwisdom.net. The wss protocol here refers to WebSockets and is designed to allow Socket.IO to work while Notes is hosted on notes.geekwisdom.net. But what about when we want to host it on a different domain?

To experiment with this problem, change the hard coded string to a different domain name then redeploy it to your server. In the JavaScript console in your browser you will get an error like this:

Refused to connect to

wss://notes.geekwisdom.net/socket.io/?EIO=3&transport=websocket&sid=x-WiqH-g6uKIqoNqAAPA

because it does not appear in the connect-src directive of the Content Security Policy.

What’s happened is that the statically defined constant was no longer compatible with the domain where Notes was deployed. You had reconfigured this setting to limit connections to a different domain, such as notes.newdomain.xyz, but the service was still hosted on the existing domain, such as notes.geekwisdom.net. The browser no longer believed it was safe to connect to notes.geekwisdom.net because your configuration said to trust only notes.newdomain.xyz.

The best solution is to make this a configurable setting by declaring another environment variable that can be set to customize behavior.

In app.mjs, change the contentSecurityPolicy section to this:

const csp_connect_src = [ “‘self'” ];

if (typeof process.env.CSP_CONNECT_SRC_URL === ‘string’

&& process.env.CSP_CONNECT_SRC_URL !== ”) {

csp_connect_src.push(process.env.CSP_CONNECT_SRC_URL);

}

app.use(helmet.contentSecurityPolicy({

directives: {

defaultSrc: [“‘self'”],

scriptSrc: [“‘self'”, “‘unsafe-inline'” ],

styleSrc: [“‘self'”, ‘fonts.googleapis.com’ ],

fontSrc: [“‘self'”, ‘fonts.gstatic.com’ ], connectSrc: csp_connect_src

}

}));

This lets us define an environment variable, CSP_CONNECT_SRC_URL, which will supply a URL to be added into the array passed to the connectSrc parameter. Otherwise, the connectSrc setting will be limited to “‘self'”.

Then in compose-swarm/docker-compose.yml, we can declare that variable like so:

services:

svc-notes:

environment:

CSP_CONNECT_SRC_URL: “wss://notes.geekwisdom.net”

We can now set that in the configuration, changing it as needed.

After rerunning the docker stack deploy command, the error message will go away and Socket.IO features will start to work.

In this section, we learned about the potential for a site to send malicious scripts to browsers. Sites that accept user-supplied content, such as Notes, can be a vector for malware. By using this header, we are able to notify the web browser which domains to trust when visiting this website, which will then block any malicious content added by malicious third parties.

Next, let’s learn about preventing excess DNS queries.

2. Using Helmet to set the X-DNS-Prefetch- Control header

DNS Prefetch is a nicety implemented by some browsers where the browser will preemptively make DNS requests for domains referred to by a given page. If a page has links to other websites, it will make DNS requests for those domains so that the local DNS cache is pre-filled. This is nice for users because it improves browser performance, but it is also a privacy intrusion and can make it look like the person visited websites they did not visit. For documentation, see https://helmetjs. github.io/docs/dns-prefetch-control.

Set the DNS prefetch control with the following:

app.use(helmet.dnsPrefetchControl({ allow: false }));     // or true

In this case, we learned about preventing the browser from making premature DNS queries. The risk is that excess DNS queries give a false impression of which websites someone has visited.

Let’s next look at how to control which browser features can be enabled.

3. Using Helmet to control enabled browser features using the Feature-Policy header

Web browsers nowadays have a long list of features that can be enabled, such as vibrating a phone, or turning on the camera or microphone, or reading the accelerometer. These features are interesting and very useful in some cases, but can be used maliciously. The Feature-Policy header lets us notify the web browser about which features to allow to be enabled, or to deny enabling.

For Notes we don’t need any of those features, though some look intriguing as future possibilities. For instance, we could pivot to taking on Instagram if we allowed people to upload photos, maybe? In any case, this configuration is very strict:

app.use(helmet.featurePolicy({

features: {

accelerometer: [“‘none'”],

ambientLightSensor: [“‘none'”],

autoplay: [“‘none'”],

camera: [“‘none'”],

encryptedMedia: [“‘self'”],

fullscreen: [“‘self'”],

geolocation: [“‘none'”],

gyroscope: [“‘none'”],

vibrate: [“‘none'”],

payment: [“‘none'”],

syncXhr: [“‘none'”]

}

}));

To enable a feature, either set it to ‘self’ to allow the website to turn on the feature, or a domain name of a third-party website to allow to enable that feature. For example, enabling the payment feature might require adding ‘paypal.com’ or some other payment processor.

In this section, we have learned about allowing the enabling or disabling of browser features.

In the next section, let’s learn about preventing clickjacking.

4. Using Helmet to set the X-Frame-Options header

Clickjacking has nothing to do with carjacking but instead is an ingenious technique for getting folks to click on something malicious. The attack uses an invisible <iframe>, containing malicious code, positioned on top of a thing that looks enticing to click on. The user would then be enticed into clicking on the malicious thing.

The frameguard module for Helmet will set a header instructing the browser on how to treat an <iframe>. For documentation, see https://helmetjs.github.io/docs/ frameguard/.

app.use(helmet.frameguard({ action: ‘deny’ }));

This setting controls which domains are allowed to put this page into an <iframe>. Using deny, as shown here, prevents all sites from embedding this content using an <iframe>. Using sameorigin allows the site to embed its own content. We can also list a single domain name to be allowed to embed this content.

In this section, you have learned about preventing our content from being embedded into another website using <iframe>.

Now let’s learn about hiding the fact that Notes is powered by Express.

5. Using Helmet to remove the X-Powered-By header

The X-Powered-By header can give malicious actors a clue about the software stack in use, informing them of attack algorithms that are likely to succeed. The Hide Powered-By submodule for Helmet simply removes that header.

Express can disable this feature on its own:

app.disable(‘x-powered-by’)

Or you can use Helmet to do so:

app.use(helmet.hidePoweredBy())

Another option is to masquerade as some other stack like so:

app.use(helmet.hidePoweredBy({ setTo: ‘Drupal 5.7.0’ }))

There’s nothing like throwing the miscreants off the scent.

We’ve learned how to let your Express application go incognito to avoid giving miscreants clues about how to break in. Let’s next learn about declaring a preference for HTTPS.

6. Improving HTTPS with Strict Transport Security

Having implemented HTTPS support, we aren’t completely done. As we said earlier, it is best for our users to use the HTTPS version of Notes. In our AWS EC2 deployment, we forced the user to use HTTPS with a redirect. But in some cases we cannot do that, and instead must try to encourage the users to visit the HTTPS site over the HTTP site.

The Strict Transport Security header notifies the browser that it should use the HTTPS version of the site. Since that’s simply a notification, it’s also necessary to implement a redirect from the HTTP to HTTPS version of Notes.

We set Strict-Transport-Security like so:

const sixtyDaysInSeconds = 5184000 // 60 * 24 * 60 * 60

app.use(helmet.hsts({

maxAge: sixtyDaysInSeconds

}));

This tells the browser to stick with the HTTPS version of the site for the next 60 days, and never visit the HTTP version.

And, as long as we’re on this issue, let’s learn about express-force-ssl, which is another way to implement a redirect so the users use HTTPS. After adding a dependency to that package in package.json, add this in app.mjs:

import forceSSL from ‘express-force-ssl’;

app.use(forceSSL);

app.use(bodyParser.json());

With this package installed, the users don’t have to be encouraged to use HTTPS because we’re silently forcing them to do so.

With our deployment on AWS EC2, using this module will cause problems. Because HTTPS is handled in the load balancer, the Notes app does not know the visitor is using HTTPS. Instead, Notes sees an HTTP connection, and if forceSSL were in use it would then force a redirect to the HTTPS site. But because Notes does not see the HTTPS session at all, it only sees HTTP requests to which forceSSL will always respond with a redirect.

These settings are not useful in all circumstances. Your context may require these settings, but for a context like our deployment on AWS EC2 it is simply not needed. For the sites where this is useful, we have learned about notifying the web browser to use the HTTPS version of our website, and how to force a redirect to the HTTPS site.

Let’s next learn about cross-site-scripting (XSS) attacks.

7. Mitigating XSS attacks with Helmet

XSS attacks attempt to inject JavaScript code into website output. With malicious code injected into another website, the attacker can access information they otherwise could not retrieve, or cause other sorts of mischief. The X-XSS-Protection header prevents certain XSS attacks, but not all of them, because there are so many types of XSS attacks:

app.use(helmet.xssFilter());

This causes an X-XSS-Protection header to be sent specifying 1; mode=block. This mode tells the browser to look for JavaScript in the request URL that also matches JavaScript on the page, and it then blocks that code. This is only one type of XSS attack, and therefore this is of limited usefulness. But it is still useful to have this enabled.

In this section, we’ve learned about using Helmet to enable a wide variety of security protections in web browsers. With these settings, our application can work with the browser to avoid a wide variety of attacks, and therefore make our site significantly safer.

But with this, we have exhausted what Helmet provides. In the next section, we’ll learn about another package that prevents cross-site request forgery attacks.

Source: Herron David (2020), Node.js Web Development: Server-side web development made easy with Node 14 using practical examples, Packt Publishing.

Leave a Reply

Your email address will not be published. Required fields are marked *