Node.js: Put a Helmet on...
With Node.js being all the buzz these days, I figured it was time to take a break from our normally scheduled Burp series and do a small sidebar on Helmet for Node.js. I’ve been working heavily in Node applications recently and realized just how common some of these “easy-fix” vulnerabilities are becoming.
Without getting into too much nitty-gritty detail, I want to go over how to set up Helmet for your Node application so that you can nip some of those easy-to-get security bugs in the bud. In the Node.js applications I’ve seen in the wild, these common vulnerabilities pop up quite a bit; when you use a platform that’s so easy to get up and running, fixing these issues may become an afterthought. Since Node.js provides you with so much freedom, it’s very easy to shoot yourself in the foot and be the target of some easy attack vectors.
Now, to be clear, Helmet is not the end-all, be-all of Node.js security–it’s just one layer of protection against a slew of issues–but it gives you a great baseline of protections that allow you to start out well.
So how does Helmet cover you?
Helmet helps protect you against the common offenses of the “low-hanging fruit” vulnerabilities, which include:
- Cross Domain protection for Flash content
- Removing the X-Powered-by header
- HSTS support
- X-Download-Options for IE8+
- Proper Cache-control headers
- Clickjacking Protection
- XSS Protection via the X-XSS-Protection header
Helmet provides most of its protection by adding headers with restrictive defaults or by removing the unnecessary ones. The npm site has documentation on how to customize the headers. Please note that the X-XSS-Protection header does not protect you from all XSS attacks and should be used with caution and in addition to security best practices for preventing XSS.
Since all of these components use headers in some way, shape, or form, they are relatively easy to see in action and understand. Helmet also provides great references to documentation so you can decide what header settings best suit your environment.
In all of the Node.js applications I’ve tested recently, at least one of these vulnerabilities is present, causing complications with compliance, security, and development progress. One aim of security is never to hold up the development process, and implementing something like Helmet requires minimal development effort and configuration. And it’s modular, so you can enable some or all of the protections.
I think the biggest win here is preventing the ad-hoc fix mentality when some of these simple vulnerabilities pop up. All too often, since many of these vulnerabilities are considered annoyances and hiccups to real development work, they are remediated in a way that does not necessarily consider future development or components that are not implemented. By implementing Helmet as middleware, all the data that enters and exits your application is seen and evaluated by Helmet, allowing you to avoid the mistake of accidentally bypassing your own security controls.
All right, so it’s a great tool. It doesn’t cover us for everything, but it’s a good baseline. Got it.
You said it was easy to install. How easy?
Helmet requires express(), which is the most common “routing framework” for Node.js thus far, so it’s likely you’ll use it, but it’s as simple as this:
Install it with NPM:
npm install helmet --save
Add your require statement to the server.js or app.js file:
var helmet = require('helmet');
If you need to customize, check out more configuration options on the official npm repo:
In my opinion, this should be one of the first considerations of a Node.js app that’s going into production. It’s not a crutch to rely on, but it is certainly a good start.
Let’s see it in action
So now that we know how easy it is to install Helmet, let’s use it on something a little more real world. I found this open source application called Node Cellar which is a great intro to backbone.js, node.js, mongodb, and express, but it doesn’t really touch on any security. It seems like a good spot for us to see just how Helmet changes behavior, and maybe we can make some other edits that will further secure the application. We’ll even implement Helmet modularly so we’re not just throwing the kitchen sink at the problem.
First, let’s take a look at some of the weaknesses without adding anything.
We can see here that when we request the root page, we get a pretty simple response with none of the protections I mentioned Helmet could provide:
This means that if we pass anything sensitive, it’ll be cached. We can also fingerprint the server since it shows us that it’s running Express, and we can see there are no clickjacking protections available. So we have some attack vectors and intelligence gathering techniques available to us.
As a quick example, here’s some simple page source to demonstrate clickjacking. This just takes the entire page and embeds it in an iframe:
In reality, this could be a complex system of layers that can trick a user into clicking somewhere they don’t intend. And here is how it manifests itself as generated content:
So let’s implement Helmet here. In the server.js file, we just add the require statement for Helmet and select the modules we want to use. In this case, we’re implementing the XSSFilter, the cache-control headers, the no-sniff option, X-Frame-Options, and we’re hiding the X-Powered-By header to reduce the attack surface and take out the easy fingerprinting for attackers ( https://github.com/relotnek/nodecellar/blob/xss-dev/server.js#L13-L17):
With the options implemented, here’s what the response looks like.
You can see that we have some extra headers set here. Our X-XSS-Protection header is on and locked down for some light XSS protection, we have all of our cache-control headers set correctly, we have our nosniff option set, and we’ve pulled out that pesky X-Powered-By header. With the SAMEORIGIN X-Frame-Options header set, we are no longer vulnerable to clickjacking (if the browser appropriately obeys that header). If we use the same simple test, we can see the page fails to load in the iframe:
So now you can fiddle with the Helmet options and observe them in BurpSuite to see how the headers affect the traffic. Hopefully, it’s helpful in implementing some protections with minimal effort.
All right, so I was about to send this post to the presses, but I ran into something in our open source application and felt like I had to mention it. Node gives us some pretty low-level access when it comes to requests and responses, and because of that, it’s very prone to cross-site scripting and injection attacks. It doesn’t really give us the automatic protection some other frameworks do.
Here’s an example in our demo application; it’s pretty straightforward. If I edit or add a new wine in Node Cellar, I can set any value I want, and there’s no real validation on the front end. Even if it did have some UI validation, it doesn’t prevent me from sending my value straight to the server with an API call. So if I drop a payload such as
<script>alert('node cellar xss')</script>
next to the Region input like so:
then every time this attribute gets rendered, it’s going to show up on the page. And this particular object is loaded on the first page of the Browse Wines page.
So what do we do? Well, the common recommendation is input encoding and output escaping, neither of which are being done here. So let’s look at a simple fix. What if every time I went to update a wine, the dirty input was cleaned prior to being saved to the database?
For that we’re going to require sanitizer, a module which simplifies an implementation of Google Caja. Like before, we add a require statement for sanitizer ( https://github.com/relotnek/nodecellar/blob/xss-dev/routes/wines.js#L2):
For the update function, we iterate through the parameters and clean them with the for loop below, before the data get saved. It’s a rough implementation, but it gets the job done ( https://github.com/relotnek/nodecellar/blob/xss-dev/routes/wines.js#L59-L61):
Now we can see that when we save the Region name (or any attribute for that matter), it’s cleaned with the sanitizer prior to updating. After I click the save button with the new code, the script tags and its contents are removed.
This takes care of scrubbing the html and encoding it on input, but what if someone is able to get dirty data into the database without using my interface? Then we’d still be in trouble. To combat this. we have to leverage some output escaping as well. The output escaping can differ depending on where and how the data is displayed. Since this small side-step into XSS was part of the bonus section, that’s just slightly beyond the scope of this article, but keep an eye out for more. I plan on digging into that next.
- Original NodeCellar Repo: https://github.com/ccoenraets/nodecellar
- Fork with Security Tweaks: https://github.com/relotnek/nodecellar
- Helmet.js: https://www.npmjs.com/package/helmet
- Sanitizer: https://www.npmjs.com/package/sanitizer