DEV Community

Matt Eddy
Matt Eddy

Posted on

Securing An Express Application

Alt Text

Overview

The focus of the article is to understand how to secure a Node.js and Express application.

Introduction

Security is a big concern in the digital world. If your application is not properly secure, then its not a matter if you get hacked, but when you get hacked. Here are a few things you can do to protect your application out in the wild.

Security Best Practices

First, lets cover some best practices from Express. Express tells us that we should disable the X-Powered-By header as it provides attackers with information about how are site works. A simple fix would be to install the package helmet. Helmet adds some out-of-the-box security changes to the application so that its less vulnerable to attacks.

const express = require("express");
const helmet = require("helmet");

const app = express();

app.use(helmet());
Enter fullscreen mode Exit fullscreen mode

Another recommendation is to avoid using the default Set-Cookie, but instead use cookie-session. The reason for this is the Set-Cookie stores the whole session object whereas the cookieSession will store only session I.D. For example we can set a cookie with cookieSession the in Node.js as such:

const express = require('express')
const cookieSession = require('cookie-session')

const app = express()
const expiryDate = new Date(Date.now() + 60 * 60 * 1000) // 1 hour
app.use(cookieSession({
  name: 'trusted cookie', // Don't use Set-Cookie
  path: '/',
  expires: expiryDate,
  keys: ['some random key'] 
}))
 ...
Enter fullscreen mode Exit fullscreen mode

To add an extra layer of security on the cookie we can change it's sameSite property. By default sameSite is set to lax if we change to strict the usage of the cookie is restricted to the domain that issued the cookie.

const express = require('express')
const cookieSession = require('cookie-session')

const app = express()
const expiryDate = new Date(Date.now() + 60 * 60 * 1000) // 1 hour
app.use(cookieSession({
  name: 'trusted cookie', // Don't use Set-Cookie
  path: '/',
  expires: expiryDate,
  keys: ['some random key'],
  sameSite: 'strict'
}))
 ...
Enter fullscreen mode Exit fullscreen mode

Next, we want to make sure our dependencies do not have security issues. We can run a npm audit, or use snyk to check for security issues in our dependencies. For example, testing dependencies with snyk will produce the following output:

Testing /Users/meddy/projects/demo...

Organization:      creativethoughtz.team
Package manager:   npm
Target file:       package-lock.json
Project name:      demo
Open source:       no
Project path:      /Users/meddy/projects/demo
Licenses:          enabled

✓ Tested 56 dependencies for known issues, no vulnerable paths found.
Enter fullscreen mode Exit fullscreen mode

Synk is one option, but we also can use just regular npm. With npm we can run a npm audit fix to scan our project for vulnerabilities and automatically install any compatible updates to vulnerable dependencies. To see the complete list of recommendations from the Express Team visit Security Best Practices. At this point the application has minimal defenses. Lets see how we can improve the application security.

Cross-Site Request Forgery (CSRF)

Cross-Site Request Forgery is one of the most common attacks used on web applications. The attack happens when a web server provides a user with an access key of some type, perhaps a cookie or token, so the user may avoid re-authentication. Once the user visits another website where a CSRF attack is set up, the malicious website will be able to make request to the server on behalf of the user. To prevent CSRF attacks use the csurf package. The csurf package ensures that all request made to the server come from your website. The csurf package enables you to store cryptographic tokens within the forms of your website. When a request is made to the server the payload must contain the token stored within the form.

Example server

const express = require('express')
const cookieSession = require('cookie-session');
const csrf = require('csurf')
const expressHandlebars = require('express-handlebars');
const bodyParser = require('body-parser')

// setup csrf Protection middleware
const csrfProtection = csrf();

const parseForm = bodyParser.urlencoded({ extended: false })

const app = express()

app.engine('handlebars', expressHandlebars({ defaultLayout: 'main' }));
app.set('view engine', 'handlebars')
const expiryDate = new Date(Date.now() + 60 * 60 * 1000) // 1 hour
app.use(cookieSession({
  name: 'session',
  path: '/',
  expires: expiryDate,
  sameSite: 'strict',
  keys: ['some random key']
}))

app.get('/form', csrfProtection, function (req, res) {
  // pass the csrfToken to the view
  res.render('send', { csrfToken: req.csrfToken() })
})
 // when a post is made verify the token
app.post('/process', parseForm, csrfProtection, function (req, res) {
  res.send('data is being processed')
})
Enter fullscreen mode Exit fullscreen mode

Basic form with _csrf token

<form action="/process" method="POST">
  <input type="hidden" name="_csrf" value="{{csrfToken}}">
  Favorite color: <input type="text" name="favoriteColor">
  <button type="submit">Submit</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Another approach to provide CSRF protection is to check the origin and referrer headers which are known as forbidden headers. Forbidden headers are headers that cannot be modified programmatically because the user agent retains full control over them. These headers contain the host from which the request was made, and we can use this information to compare that to the host of our application.
Alt Text
This will help provide an extra layer of security against CSRF attacks. Lets continue exploring other security options for our application.

Cross-Site Scripting (XSS)

Cross-Site Scripting is when an attacker is able to inject malicious code within your application. The good news is that if you're using a frontend framework such as Angular, React, or Pug then your data will be sanitized by the framework protecting you from XSS. However, the only way to ensure the data within the database is sanitized is by sanitizing the data on the server. We can use the package sanitize-html to sanitize data.

const sanitizeHtml = require('sanitize-html');
const dirty = 'some really tacky <script>alert("Hi")</script>';
const clean = sanitizeHtml(dirty);
Enter fullscreen mode Exit fullscreen mode

You can take this approach if you are not using a framework to render your frontend or if you do want any html tags stored within database. To learn more about the different types of XSS attacks and how to prevent them check out the OWASP Cheat Sheet.

Rate Limiting

Rate Limiting is another defensive mechanism we can deploy to protect our resources from exploitation. Rate Limiting will limit the number of request that can be made to the sever. When a maximum number of request have been reached the server will limit request from that source.

Authorization

Authorization represent the privileges of a user on our system. The privileges are in reference to a particular resource, and are defined by the acronym CRUD, which stands for create, read, update, and delete. When determining the privileges of a user the rule of thumb is Principle of least privilege. This means you should grant privileges only as needed to the users of the system.

Authentication

Password Authentication Protocol (PAP) is one of the weakest authentication schemes, yet the most used. Passwords are easily hacked, and even worse their daisy-chained. The problem arises from the fact the average users has over 90 online accounts. Therefore, if our application requires a password for authentication, then the application should enforce strong password requirements. This will help ensure that our authentication scheme is not the weakest in the chain. Also, we should consider the encryption algorithm for passwords to prevent password cracking. When choosing a hashing algorithm we should avoid encryption methods using the sha2 hashing algorithm and instead use methods that use the argon2 hashing algorithm as it is more secure.

hashing code snippet

const argon2 = require('argon2');

try {
  const hash = await argon2.hash("password");
} catch (err) {
  //...
}
Enter fullscreen mode Exit fullscreen mode

verify password code snippet

try {
  if (await argon2.verify("<big long hash>", "password")) {
    // password match
  } else {
    // password did not match
  }
} catch (err) {
  // internal failure
}
Enter fullscreen mode Exit fullscreen mode

If it is possible we should avoid building our own authentication system, and look to leverage an existing authentication system. One popular authentication system is passport. Passport provides us with options for authentication. We can delegate the entire authentication process to use OAuth, or SAML standards, or if we want to manage authentication ourselves we can use a Local strategy.

passport.use(new LocalStrategy(
  function(username, password, done) {
    User.findOne({ username: username }, function (err, user) {
      if (err) { return done(err); }
      if (!user) { return done(null, false); }
      if (!user.verifyPassword(password)) { return done(null, false); }
      return done(null, user);
    });
  }
));
Enter fullscreen mode Exit fullscreen mode

Another concept gaining popularity is passwordless authentication. Passwordless authentication allow users to log in without the need to remember a password. Instead, users enter their mobile number or email address and receive a one-time code or link, which they can then use to log in. We now have a few options for authenticating users on the web, lets continue increasing the security of the application.

HTTPS (Data In Transit)

HTTPS is probably one of the simplest security mechanism you can employ to protect the integrity of your data. Https encrypts the data while it is in transit, making it extremely difficult for hackers to gain access to the information being exchange between client and server.

AES-256 (Data At Rest)

Another security feature we can use to protect our application and resources is to encrypt the data while its stored in the database or at rest. A strong encryption algorithm such as AES-256 can be used to encrypt data at rest. One popular approach, that uses AES-256, to encrypt data at rest is AWS KMS Envelope Encryption Strategy. The scheme uses a master key to encrypt a data key, which the data key can then be used to encrypt the data at rest. When we want to decrypt our data we must use the same data key that was used to encrypt the data at rest.

Have A Plan

Having a security plan will be the ultimate determinant of your security initiatives and survival of your application. Knowing what to do, who to notify, the type of attack, and how to respond is something outlined in a security plan. A security plan is usually something produce by a security team, which is outside the scope of this article. However, the AWS Security Whitepapers outlines some of the best security practices in the industry, many which they use in their own software projects.

Conclusion

As always, take care, and thank you for reading this article. If you found this article helpful please leave a rating or comment, or if you have question don't hesitate to ask.

Oldest comments (0)