AUTO1 Group

Secure coding practices for NodeJS Web Applications

By Rajababu Pradhan

Raja is Senior Frontend Engineer at AUTO1 Group.

< Back to list
Coding Jun 20 2018

Secure coding practices for NodeJS Web Applications

Everyone would agree that security is very important and with the increase in cyber attacks in the recent past, organizations are investing heavily on application security. In this post lets talk about securing a web application built in NodeJS.

NOTE: The security concepts discussed here are language or framework agnostic. However, here we will see how these practices are implemented in NodeJS web applications.

1. Use SSL/TLS for communication

It is always a good practice to send your data over HTTPS rather than HTTP and it is imperative if your app transmits sensitive data. Encrypting data transmitted between the client and server helps mitigate several attacks like man-in-the-middle(MITM) attack, packet sniffing, eavesdropping etc. Let’s see how to set up TLS/SSL in Express 4.x:

Lets first generate a self-signed certificate:

$ openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365

This generates a self-signed certificate valid for 365 days.

NOTE: The self-signed certificate is not ideal for production. For production, you should get a certificate from a Certificate Authority(CA).

Next, enable HTTPS on Express. Additionally, redirect all HTTP traffic to HTTPS:

const fs = require('fs');
const https = require('https');
const express = require('express');

const NODE_ENV = process.env.NODE_ENV || 'development';
const PORT = process.env.PORT || 3443;

const app = express();

https.createServer({
  key: fs.readFileSync('/path/to/key.pem'),
  cert: fs.readFileSync('/path/to/cert.pem')
}, app).listen(PORT);

// Redirect http requests to use https in production
if (NODE_ENV === 'production') {
  app.use((req, res, next) => {
    if (req.header('x-forwarded-proto') !== 'https') {
      res.redirect(`https://${req.header('host')}${req.url}`);
    } else {
      next();
    }
  });
}

2. Use security headers generously

i) Strict-Transport-Security: The HTTP Strict Transport Security(HSTS) if set in the response header, tells the browser that it should only communicate using HTTPS instead of HTTP while communicating with the specified domain.

Syntax:

Strict-Transport-Security: max-age=<expire-time>

Here, max-age is the time(in secs) that the browser should remember that this site is only to be accessed using HTTPS.

Example: from facebook.com:

strict-transport-security:max-age=15552000;

ii) X-Frame-Options: This HTTP response header can be used to indicate whether or not a browser should be allowed to render a page in a <frame>, <iframe> or <object>. Sites can use this to avoid clickjacking attacks, by ensuring that their content is not embedded into other sites.

Syntax:

X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN
X-Frame-Options: ALLOW-FROM https://siteutrust.com/

iii) X-XSS-Protection: This HTTP response header enables the built-in XSS filter in modern browsers.

Example:

X-XSS-Protection: 1

iv) X-Content-Type-Options: This response HTTP header is a marker used by the server to indicate that the MIME types advertised in the Content-Type headers should not be changed and be followed. This prevents MIME type sniffing attacks.

Syntax:

X-Content-Type-Options: nosniff

v) Content-Security-Policy: Prevents a range of injection attacks including Cross Site Scripting(XSS) attack.

Syntax:

Content-Security-Policy: policy

For a detailed explanation of CSP, go through this link.

To set these headers in NodeJS, use the helmet npm package:

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

const app = express();

<b>app.use(helmet())</b>

This sets all the necessary headers in response.

To set the headers individually:

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

For an exhaustive list of security headers that should be set in a web application, check out the OWASP Secure Headers Project.

NOTE: In some web servers, the security headers can be set in the server configuration file itself. For example, in nginx server, we can set the above headers in nginx.conf as shown below:

add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection 1;
add_header Content-Security-Policy "default-src 'self'";

3. Preventing CSRF attacks

Cross site request forgery (CSRF), also known as XSRF, Sea Surf or Session Riding, is an attack vector that tricks a web browser into executing an unwanted action in an application to which a user is logged in. CSRF attacks specially targets state-changing requests and can force the victim to transfer funds, change email/password and so on.

CSRFs are typically conducted using social engineering, such as an email or link that tricks the victim into sending a request to a server on behalf of the attacker. The server has no way to distinguish a forged request from a genuine one.

In NodeJS, to prevent CSRF attack, we usually use the csurf express middleware:

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

const csrfProtection = csrf({ cookie: true });
const parseForm = bodyParser.urlencoded({ extended: false });

// create express app
const app = express();

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

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

In the view use the CSRF token passed:

<form action="/process" method="POST">
  <input type="hidden" name="csrf_token" value="{{csrfToken}}">

  Enter amount: <input type="number" name="amount">
  <button type="submit">Submit</button>

</form>

4. Preventing XSS attacks

Cross-Site Scripting (XSS) attacks are a type of injection, in which malicious scripts are injected into otherwise benign and trusted web sites. An attacker can use XSS to send a malicious script to an unsuspecting user. The end user’s browser has no way to know that the script should not be trusted, and will execute the script. Because it thinks the script came from a trusted source, the malicious script can access any cookies, session tokens, or other sensitive information retained by the browser and used with that site. These scripts can even rewrite the content of the HTML page.

The thumb rule to prevent this category of attack is to always validate and sanitize user data before processing or storing in database. Never trust data coming from user. Validation must be done on the server-side as client-side validation can be easily bypassed using tools such as Burp Suite, TamperData etc.

A common approach to validate and sanitize user data is to use a library like validator.js.

Example: To validate an email

import validator from 'validator';

if(validator.isEmail('foo@bar.com')) {
  // Process email or store in DB
}

This library provides a number of validators and sanitizers to filter user inputs.

Other useful libraries include DOMPurify and xss-filters.

Here is an example to sanitize user input using xss-filters:

const express = require('express');
const app = express();
const xssFilters = require('xss-filters');

app.get('/', (req, res) => {
  let firstname = req.query.firstname; //an untrusted input
  res.send('<h1> Hello, ' + xssFilters.inHTMLData(firstname) + '!</h1>');
});

app.listen(3000);

5. Preventing SQL Injection(SQLi) attacks

Passing unvalidated user input directly to a SQL statement is vulnerable to SQL injection attack.

Conside the following example:

// SQL query vulnerable to SQLi
sql = "SELECT * FROM users WHERE username='" + username + "' AND password='" + password + "'";

// Execute the SQL statement
database.execute(sql)

Now suppose the user enters the following in the username field:

' OR '1'='1' --

The above SQL statement becomes:

sql = "SELECT * FROM users WHERE username='" + ' OR 1=1 -- + "' AND password='" + password + "'";

This effectively nullifies the need of a password and returns all the users in the database.

This attack can be completely prevented by using parametrized or prepared statement.

If you’re using an ORM to access the database (Mongoose, Sequelize etc), the ORM will normally take care of SQL injection by using prepared statements under the hood.

6. Secure cookies using cookie flags

XSS vulnerability in an application can be used to steal browser cookies. To prevent cookie stealing we can set the httpOnly flag of the cookie. Additionaly, we can tell the browser to send cookies only over HTTPS using the secure flag.

secure : this attribute tells the browser to only send the cookie if the request is being sent over HTTPS.

HttpOnly : this attribute is used to help prevent attacks such as cross-site scripting, since it does not allow the cookie to be accessed via JavaScript.

Example:

app.use(session({
  secret: ‘My super secret’,
  <b>cookie: { httpOnly: true, secure: true }</b>
}));

7. Preventing brute force and DoS attack

To prevent our site from overwhelming with a large number of requests, we need to put some kind of rate limiting to our API.

We can use the ratelimiter npm package to implement rate limiting. If you are using Express, the express-rate-limit middleware can be used as shown below:

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

const 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);

8. Error Handling

Any error in the application should be handled gracefully by showing a custom error page to the user instead of showing stack trace in the error page thereby leaking sensitive infrastructure information like server info.

9. Tools of the trade

i) The Node Security Project To check the various npm modules for known vulnerabilities, the Node Security Project provides the nsp tool to check for vulnerabilities:

$ nsp check

ii) Synk Synk checks the application against Snyk’s open source vulnerability database for any known vulnerabilities in our dependencies.

$ npm install -g snyk
$ cd your-app
$ snyk test

iii) nmap Nmap (“Network Mapper”) is a free and open source utility for network exploration or security auditing.

iv) sqlmap sqlmap is an open source penetration testing tool that automates the process of detecting and exploiting SQL injection flaws and taking over of database servers.

v) Burp Suite The must-have tool for application penetration testing. It includes an automated scanner to detect most common vulnerabilities in a web application.

Further Readings

OWASP Top 10 Most Critical Web Application Security Risks

NodeJS Security Checklist

Stories you might like:
CodingJul 2
By Nicholas Peretti

Create forms at scale with Formik and Yup

CodingJun 24
By Wojciech Oroński

Yet another case study of developing serverless apps with PHP.

CodingApr 4
By Chirag Swadia

How we use ES6 generators instead of thunk to simplify our React Redux application code and...