The Nodesource Blog

#shoptalk Subscribe

9 Security Tips to Keep Express from Getting Pwned

Security is really hard to get right. There are so many different factors to consider, countless different ways to break an application.

This is just as true with Express applications as it is with any other web framework. There's no instant way to make sure an application won't be taken down by a Denial of Service (DoS) attack because of how it handles one type of user input, or how it routes a specific request request.

We've made this list of 9 tips that will help harden Express applications against a suite of different kinds of security vulnerabilities.

This guide is definitely not meant to address every single possible security flaw within an Express application. It does, however, provide a basic checklist to ensure that an Express application addresses some of the biggest security threats.

1. Enable TLS/SSL

If an application has any dynamic parts (HTTP methods like POST, PUT, and DELETE) which includes anything from logging into a banking site to sending a tweet, that mutate the information from the client, using HTTPS is a vital implementation for ensuring that information isn’t modified in transit.

Here is an example of a route that performs data mutation on the server:

const express = require('express');
const db = require('our-db-of-choice');

const app = express();

app.put('/profile/update', function(req, res, next){
  if(!req.session || !req.session.userId) {
    return res.status(403).send({ok: false});
  }

  const userData = sanitize(req.body);

  db.update(req.session.userId, userData, function(err){
    if(err) return res.status(500).send({ok: false});
    else return res.status(200).send({ok: true});
  })

});

Cost use to be an easy excuse to not invest in a SSL certificate. Thankfully, that's no longer a valid point with the new, completely free SSL certificate resources. Namely, Let’s Encrypt has driven the idea of "free, automated, and open" certificates with great success.

That said, we highly recommend that your Node.js application isn’t directly exposed to the internet and SSL termination is handled prior to Node.js. The lack of direct access to the Node.js application will add another layer of security. Using NGINX to do this is one option that we highly recommend. It can terminate SSL more efficiently than a Node.js application can.

When configuring SSL termination above Node.js, will need to make sure the proxy being used is added as trusted:

// Where 1.0.0.0 is the IP address of your Proxy
app.set(‘trust proxy’, ‘1.0.0.0’);

Additionally, make sure the proxy in use sets the X-Forwarded-Proto: https HTTP header.

Learn about trust proxy values in the Express Docs section for trust proxy, and see the official guide on how to put an Express app behind a proxy.

Testing HTTPS Certificate Transmission

Once HTTPS is set up, certificate transmission can be easily validated with nmap, sslyze, or OpenSSL. How?

Using Qualys SSL Labs

Visit the Qualys SSL Labs web service. Enter your domain and you're off!

Using nmap

Visit the nmap site to learn more about nmap.

How to run nmap to validate SSL transmission:

nmap --script ss-cert,ssl-enum-ciphers -p 443 example.com

Using sslyze

Visit the sslyze repo to learn more about sslyze.

How to run sslyze to validate SSL transmission:

sslyze.py --regular example.com:4444

Using OpenSSL

Visit the OpenSSL site to learn more about OpenSSL.

How to use OpenSSL to validate SSL transmission:

echo ‘q’ | openssl s_client -host example.com -port 443


2. Check for Known Security Vulnerabilities

There are a few tools in the Node ecosystem that allow easy checking for vulnerabilities in Node and Express application dependencies. These tools are highly valuable in ensuring that no vulnerabilities are currently in the packages an application relies on, and none are added into that application when its packages are updated.

3. Encode All Untrusted Data Sent to an Application

Encoding any data - be it URLs and Express routes, HTML body and attributes user submitted data, CSS attributes - is an extremely important part of hardening the security of an application. Many assume that their templating library does this well enough by default - but assumptions can lead to disastrous results.

By just implementing a few packages to sanitize data within an Express application, developers can feel more secure in knowing that an application has been hardened against many of the more obscure and strange vectors of attack that templating libraries might not be protecting against.

HTML Encoding

Note: When using the escaped value within a tag, it is only suitable as the value of an attribute, where the value is quoted with either a double quote character (") or a single quote character (').

CSS Encoding

JavaScript Encoding

URL and URI Encoding

To read a bit more about the high value of encoding user input, take a look at the XSS Prevention Cheat Sheet by OWASP.

4. Prevent Parameter Pollution to Stop Possible Uncaught Exceptions

While there is no defined standard for handling multiple parameters with the same name the defacto standard across frameworks is to treat the values as an array.

This can be tricky since Express behavior for a single name is to return it as a String, when multiple are pass the type is changed to an Array. If this isn't accounted for in query handling, an application will emit an uncaughtException event that can bring the whole thing down, making this problem a potential DoS vector if not appropriately addressed.

Not preparing Express applications for this simple vulnerability can expose the server to a Denial of Service attack.

Here’s an example of the vulnerable request:

1: Send a request with two values for the same key.

curl http://example.com:8080/endpoint?name=Itchy&name=Scratchy

2: The Express server expects the name key to be a String, and uses .toUpperCase() on it.

app.get('/endpoint', function(req, res){
  if(req.query.name){
    res.status(200).send('Hi ' + req.query.name.toUpperCase())
  } else {
    res.status(200).send('Hi');
  }
});

The code example assumes that req.query.name is a String type. But, since there are two arguments with the same name Express returns the results as an Array: ['Itchy', 'Scratchy']. This will throw an Error that will crash an Express application.

To ensure that an application won’t fail this way, the OWASP article Testing for HTTP Parameter pollution is an awesome guide on how to actually test an application thoroughly against this type of attack.

5. Add Helmet to Set Sane Defaults

The Helmet package is a collection of security modules that prevent a variety of attacks against an Express applications - it’s one easy, drop-in package that hardens Express by adding just two lines to an application.

The a basic example of an Express application using Helmet:

const express = require(“express”);
const helmet = require('helmet');

const app = express();

app.use(helmet());

It includes a whopping 10 packages that all work to block malicious parties from breaking or using an application to hurt its users.

Here's the full list of packages Helmet enables and makes configurable with just two added lines:

  1. helmet-csp - Enables the Content-Security-Policy HTTP header. This defines the trusted origins (sources) of content - such as scripts, images, and other types of content - that is allowed to load in a webpage.

  2. dns-prefetch-control - DNS Prefetching is generally good for speeding up load times, especially on mobile devices. Disabling Prefetching can limit potential data leakage about the types of external services an application uses. In addition, disabling can reduce traffic and costs associated with DNS query lookups.

  3. frameguard - Enables the X-Frame-Options HTTP header. This blocks clickjacking attempts by disabling the option for the webpage to be rendered on another site.

  4. hide-powered-by - Removes the X-Powered-By HTTP header. This blocks one route of easy identification of an application being run with Express as a web server - which can lead to specific targeting of Express and Node issues.

  5. hpkp - Enables Public Key Pinning headers, which can prevent MITM (man in the middle) attacks that use forged certificates.

  6. hsts - Enables the Strict-Transport-Security header, which forces all connections to the server to use HTTPS, instead of using unsecured HTTP.

  7. ienoopen - Enables the X-Download-Options HTTP header, with the configuration of noopen, to prevent Internet Explorer users from executing downloads within an application's context.

  8. nocache Enables four HTTP headers - Cache-Control, Pragma, Expires, and Surrogate-Control - with defaults that block the client caching old versions of site resources.

  9. dont-sniff-mimetype - Enables the X-Content-Type-Options HTTP header to stop clients from sniffing the MIME-type of a response outside of the content-type that is declared.

  10. x-xss-protection - Enables the X-XSS-Protection HTTP header that prevents some XSS attacks in a set of more recent browsers.

If a more advanced configuration - such as setting specific allowances on the different security packages - is desirable, visit the README.md in the Helmet repo to learn more about how to accomplish that effectively.

Be sure to checkout the doc for more advanced configuration

6. Tighten Session Cookies

Express has default cookie settings that aren’t highly secure. They can be manually tightened to enhance security - for both an application and its user.

  • secret - A secret string for the cookie to be salted with.
  • key: The name of the cookie - if left default (connect.sid), it can be detected and give away that an application is using Express as a web server.
  • httpOnly - Flags cookies to be accessible by the issuing web server, which assists in preventing session hijacking.
  • secure - Ensure that it is set to true - which requires TLS/SSL - to allow the cookie to only be used with HTTPS requests, and not insecure HTTP requests.
  • domain - Indicates the specific domain that the cookie can be accessed from.
  • path - indicates the path that the cookie is accepted on within an application's domain.
  • expires - The expiration date of the cookie being set. Defaults to a session cookie. When setting a cookie, the application is storing data on the server. If a timely expiration is not set up on the cookie, the Express application could start consuming resources that would otherwise be free.

A basic example setup of how to use express-session to securely set cookies:

const express = require('express');
const session = require('express-session');

const app = express();

app.use(session({  
  secret: 'mySecretCookieSalt',
  key: 'myCookieSessionId', 
  cookie: {
    httpOnly: true,
    secure: true,
    domain: 'example.com',
    path: '/foo/bar',
    // Cookie will expire in 1 hour from when it's generated 
    expires: new Date( Date.now() + 60 * 60 * 1000 )
  }
}));


7. Block Cross-Site Request Forgeries

An attacker can attempt to put data into an application via their own site through a common phishing technique that uses Cross-site request forgeries. An attacker making a phishing attempt can create a request via a form or other input that creates a request against an application, through the forms, data, or other input an application has exposed.

This can be mitigated with a CSRF token implementation - essentially, every time the user makes a request a new CSRF token is generated and added to the user’s cookie. To effectively prevent against CSRF attacks, that token should be added as a value to inputs in an application’s templates and will be checked against the token that the CSRF library, such as csurf generated when the user sends information.

Setting up Express to use csurf:

const express = require(‘express’);  
const csrf = require('csurf');

const app = express();

app.use(csrf());

app.use(function(req, res, next){ 
 // Expose variable to templates via locals
 res.locals.csrftoken = req.csrfToken(); 
 next();
});

Setting a value of the csrf token in an application's templates:

<input type="hidden" name="_csrf" value={{csrftoken}} />

Note: {{csrftoken}} is Handlebars syntax - this will differ slightly in other templating languages.

8. Don't Use Evil Regular Expressions

Many of commonly occurring vulnerabilities in Node.js packages and applications are often the result of poorly formed Regular Expressions. How can bad, or _evil_, regex patterns be identified?

Evil Regexes

A regex is called "evil" when it can take exponential time when applied to certain non-matching inputs.

Evil Regex patterns contains:

  • Grouping with repetition
  • Inside the repeated group:
  • Repetition
  • Alternation with overlapping

Examples of Evil Patterns:

  • (a+)+
  • ([a-zA-Z]+)*
  • (a|aa)+
  • (a|a?)+
  • (.*a){x} | for x > 10

All the above are susceptible to the input aaaaaaaaaaaaaaaaaaaaaaaa! (The minimum input length might change slightly, when using faster or slower machines).

This repetition can be a massive hinderance to an application’s performance - a regex that would be expected to execute in milliseconds could be exploited to take seconds - even minutes - to complete. This will completely stop an application from running, as it blocks the Node.js event loop from running any other code. Effectively, these kinds of regex freeze the server.

Tools for auditing Regular Expressions:

  • RXRR - Regular expression denial of service (REDoS) static analysis.
  • SDL RegEx Fuzzer - a tool to assist in testing regex for possible DoS vulnerabilities.

For more information, the Regular expression Denial of Service - ReDoS article by OWASP is a good starting point.

9. Add Rate Limiting

When all else fails, ensuring that an Express application has a sane solution for rate limiting - like that set by express-limiter - which effectively blocks an IP address from making an outrageous number of requests - will assist in blocking an overzealous user or out-of-control bot that could accidentally crash an application.

Setting up express-limiter with redis requests in an Express application:

const express = require('express');
const redisClient = require('redis').createClient();

const app = express();

const limiter = require('express-limiter')(app, redisClient);

// Limit requests to 100 per hour per ip address.
limiter({
  lookup: ['connection.remoteAddress'],
  total: 100,
  expire: 1000 * 60 * 60
})


One last thing…

If you're interested in learning more about Node.js and similar topics like Express, application security, npm, Electron, JavaScript, Kubernetes, Docker, and tons more, you should follow @NodeSource on Twitter. We're always around, and would love to hear from _you_!