13 best practices to secure your NodeJS web application

Everyone agrees that web application security is very important but few take it seriously. Here’s a 13-step security checklist that you should follow before deploying your next web application.

I have added links to some npm modules which assist in solving some of these problems, where appropriate.

1. Use SSL for communication

If there’s one thing that your web application should do, it’s this. Ensure that all communication with your server goes through SSL. This ensures that communication between a user and your server is encrypted.

Normally, if you are using nginx or some load balancer, SSL will be terminated before it hits your application. However, if you’re just using an Express server, it can still support SSL. Check out this StackOverflow response, which links to the relevant documentation on how to set up SSL on your Express server.

You can get an SSL certificate for free from StartSSL LetsEncrypt, so there’s no reason for you to not have an SSL Cert. (ipgof notes that StartSSL certs have started to be distrusted by Mozilla).

Yes, I’m aware my blog doesn’t have SSL and I’m working on it.

2. Always escape user data

Ensure that data that users input through forms is escaped. There are different ways of doing this. A common approach is to use a sanitizer like Validator.js, which ensures that your forms submit correct datatypes.

On the server-side, you should never directly input data into a raw database query. More on this below.

3. Use prepared statements for database queries

Creating database requests where you directly pass user information into a statement is a recipe for disaster. Instead, consider using prepared statements.

A prepared statement is a template created by the application and sent to the database. Certain values are left unspecified.

INSERT INTO PRODUCT (name, price) VALUES (?, ?)

Prepared statements are resilient against SQL injection, because parameter values, which are transmitted later using a different protocol, need not be correctly escaped. If the original statement template is not derived from external input, SQL injection cannot occur.

If you’re using an ORM to access the database (Mongoose, Sequelize, Waterline, etc), the ORM will normally take care of SQL injection by using prepared statements under the hood. Check your ORM’s documentation to see if they do this.

4. Remove sensitive information from Request URLs

When building web applications, we normally follow RESTful conventions when displaying data to the user. For example, you may have a user profile page, and the URL may be something like this:

/view/users/:userId

In this case, we are publicly displaying the userId to the end user. While that may be fine, there may also be reason to hide these IDs. Similarly, you can imagine scenarios where public URLs disclose sensitive information.

There’s no way to check for this programatically, but you should go through your routes definitions to double-check that your routes and query strings do not display sensitive information.

5. Allow redirects only to whitelisted or hardcoded URLs

Do not have a server-side redirect that redirects somewhere based on user input. Instead, every redirect should be to a hardcoded URL. This makes it easy to test and ensure that redirects go where you expect them to.

6. Disable all unused API routes

Before deploying your web application to production, ensure that all your API routes are being used, and disable any that are unused or unprotected. This is especially important if you are using a library that automatically generates REST API endpoints, like Sails or Feathers.

7. CSRF Token should be present in all pages that create or update data

Cross-Site Request Forgery (CSRF) is an attack that forces a user to execute unwanted actions on a web application in which they’re currently logged in.

Ensure that all your pages have CSRF protection, or atleast all pages that create or update data (pages that call non-GET API endpoints).

In Node.js, you can use the csurf module which provides CSRF middleware for Express applications.

var cookieParser = require('cookie-parser')
var csrf = require('csurf')
var bodyParser = require('body-parser')
var express = require('express')

// setup route middlewares 
var csrfProtection = csrf({ cookie: true })
var parseForm = bodyParser.urlencoded({ extended: false })

// create express app 
var app = express()

// parse cookies 
// we need this because "cookie" is true in csrfProtection 
app.use(cookieParser())

app.get('/form', csrfProtection, function(req, res) {
  // pass the csrfToken to the view 
  res.render('send', { csrfToken: req.csrfToken() })
})

app.post('/process', parseForm, csrfProtection, function(req, res) {
  res.send('data is being processed')
})
Inside the view (depending on your template language; handlebars-style is demonstrated here), set the csrfTokenvalue as the value of a hidden input field named _csrf:
<form action="/process" method="POST">
  <input type="hidden" name="_csrf" value="{{csrfToken}}">
  <input type="text" name="email">
  <button type="submit">Submit</button>
</form>

If you’re using an MVC Framework like Bedrock or Sails, CSRF protection is usually built in. Read the project documentation to figure out how to enable it.

8. Provide log off or exit functionality which expires session

Simple, but often overlooked. Ensure there is a way for users to log off your site and expire any sessions. People could be using your web application from public computers. If you’re using a user authentication system such as PassportJS, this is often trivial:

app.get('/logout', function(req, res){ 
  req.logout(); //provided by passport
  res.redirect('/'); 
});

Importantly, test this to make sure sessions and cookies are being removed as expected.

9. HTTP Only Cookie Attribute for all pages and links

This is another issue related to preventing XSS vulnurabilities. Tagging a cookie with the HttpOnly flag tells the browser that this particular cookie should only be accessed by the server. A malicious user will not be able to access the cookie from JavaScript.

Implementing this in a Node application should be pretty easy. Just update your session configuration:

app.use(session({  
  secret: 'My super session secret',  
  cookie: {  httpOnly: true,  secure: true  } 
}));

There’s a great article on Coding Horror that dives into HTTP-Only Cookies in more detail. I recommend reading it.

10. AUTOCOMPLETE=off attribute for sensitive HTML form input fields

Another simple, yet overlooked aspect of security. In your HTML forms, you should ensure that any sensitive input fields have the HTML attribute autocomplete=off.

<input type="email" name="email" autocomplete="off"/>

This prevents the browser from automatically setting certain attributes. You shouldn’t set this for every input element as it does harm the user experience. Just be judicious and think through the use cases.

11. Set your X-Frame-Options set to DENY, SAMEORIGIN, or ALLOW-FROM

The X-Frame-Options HTTP response header can be used to indicate whether or not a browser should be allowed to render a page in a <frame> or <iframe>. Sites can use this to avoid Clickjacking attacks, by ensuring that their content is not embedded into other sites. Set the X-Frame-Options header for all responses containing HTML content. The possible values are “DENY”, “SAMEORIGIN”, or “ALLOW-FROM <url>”.

Generally, unless you have a good reason to allow your web application to be viewed via an iframe, it’s best to set X-Frame-Options: DENY.

In a Node Application, you can do this using a module like Helmet, which provides multiple security HTTP headers, including this fix.

var express = require('express')
var helmet = require('helmet')
var app = express()

app.use(helmet({
  frameguard: {action: 'deny'}
}))

12. Set Security HTTP Headers

Following on from the previous point, there are a few other HTTP headers which you should set for your application.

Helmet can help with all of these, so I recommend that.

13. Protect from Brute Force and DDOS Attacks

To prevent your site from being bombarded by a large set of requests and subsequently crashing, you should build in some type of rate limiting to all your requests.

If you are building a Node application with Express, you can use the express-rate-limit middleware. The ratelimiter npm module is also good, but it has a Redis dependency.

var RateLimit = require('express-rate-limit');

app.enable('trust proxy'); // only if you're behind a reverse proxy (Heroku, Bluemix, AWS if you use an ELB, custom Nginx setup, etc) 

var limiter = new RateLimit({
  windowMs: 15*60*1000, // 15 minutes 
  max: 100, // limit each IP to 100 requests per windowMs 
  delayMs: 0 // disable delaying - full speed until the max limit is reached 
});

//  apply to all requests 
app.use(limiter);
Up Next:

How to send emails in Sails & Bedrock

How to send emails in Sails & Bedrock