//

NODE JS SECURITY

Node.js has gained momentum in the recent years but there are not many security guidelines available out there.

In this article we will go through some of the key facts to keep in mind when you are implementing a Node.js application.

The areas of security we will cover are:

  • Authentication
  • Configuration Management
  • CSRF
  • Data Validation
  • Denial of Service
  • Error Handling
  • HTTP Strict Transport Security (HSTS)
  • Node Package Manager (NPM)
  • Secure Transmission
  • Session Management

Authentication

Brute Force Protection

To throttle invalid login attempts as well as to prevent online cracking and to reduce unnecessary database calls, we could implement a solution similar to the following:

var failedIPs = {},
  TEN_MINS = 600000,
  THIRTY_MINS = 3 * TEN_MINS;;

function tryToLogin() {
    var failed = failedIPs[remoteIp];
    if (failed && Date.now() < failed.nextAttemptTime) {
        return res.error(); // Throttled. Can't try yet.
    } else {
      // Otherwise do login
    }
}

function onLoginFail() {
    var failed = failedIPs[remoteIp] = failedIPs[remoteIp] ||
      {
        count: 0,
        nextAttemptTime: new Date()
      };

    ++failed.count;
    // Wait another two seconds for every failed attempt
    failed.nextAttemptTime.setTime(Date.now() + 2000 * failed.count);
}

function onLoginSuccess() {
  delete failedIPs[remoteIp];
}

// Clean up people that have given up
setInterval(function() {
    for (var ip in failedIPs) {
        if (Date.now() - failedIPs[ip].nextAttemptTime > TEN_MINS) {
            delete failedIPs[ip];
        }
    }
}, THIRTY_MINS);

Alternativley, you can use express-brute, a brute-force protection middleware for express routes that rate-limits incoming requests, increasing the delay with each request in a fibonacci-like sequence.

var ExpressBrute = require('express-brute'),

var store = new ExpressBrute.MemoryStore();
var bruteforce = new ExpressBrute(store);

app.post('/auth',
    bruteforce.prevent,
    function (req, res, next) {
        res.send('Success!');
    }
);

Configuration Management

Your site should set the following HTTP headers:

  • Content-Security-Policy prevents a wide range of attacks, including Cross-site scripting and other cross-site injections
  • Strict-Transport-Security enforces secure (HTTP over SSL/TLS) connections to the server
  • X-Content-Type-Options prevents browsers from MIME-sniffing a response away from the declared content-type
  • X-Frame-Options provides clickjacking protection
  • X-XSS-Protection enables the Cross-site scripting (XSS) filter built into most recent web browsers
Clickjacking

Clickjacking, also known as a UI redress attack, is when an attacker uses multiple transparent or opaque layers to trick a user into clicking on a button or link on another page when they were intending to click on the top level page. Thus, the attacker is hijacking clicks meant for their page and routing them to another page, most likely owned by another application, domain, or both.

Node.js

You can easily set these using Helmet module.

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

app.use(helmet());

Nginx

In most architectures these headers can be set in web server configuration (Apache, nginx), without changing actual application's code. In nginx it would look something like this:

# nginx.conf

add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Content-Security-Policy "default-src 'self'";
Check your site for necessary header

http://cyh.herokuapp.com/cyh

Sensitive Data on the Client Side

Make sure when you are deploying applications, you never expose API secrets and credentials in your source code.

You can avoid this by regular code reviews, and use of pull requests.

CSRF

Cross-Site Request Forgery (CSRF) is a type of attack that occurs when a malicious Web site, email, blog, instant message, or program causes a user's Web browser to perform an unwanted action on a trusted site for which the user is currently authenticated.

We can use csrf npm module to create custom CSRF middleware to deal with this kind of attacks.

'use strict';

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

var app = express();

// 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');
});

While on the view layer you have to use the CSRF token like this:

<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>

Data Validation

SQL Injection

SQL injection is a code injection technique, used to attack data-driven applications, in which malicious SQL statements are inserted into an entry field for execution (e.g. to dump the database contents to the attacker).

Let's look at an example:

select title, author from books where id=$id

In this example $id is coming from the user - what if the user enters 2 or 1=1? The query becomes the following:

select title, author from books where id=2 or 1=1

The easiest way to defend against these kind of attacks is to use parameterized queries or prepared statements.

$stmt = $db->prepare("select title, author from books where id=?");
$stmt->bindParam(1, $id);

$id = 1;
$stmt->execute();

Denial of Service (DoF)

A denial-of-service (DoS) attack is an attempt to make a machine or network resource unavailable to its intended users, such as to temporarily or indefinitely interrupt or suspend services of a host connected to the Internet. This could result in one of the few issues below:

Account Lockout

In an account lockout attack, an attacker attempts to lockout user accounts by purposely failing the authentication process as many times as needed to trigger the account lockout functionality. This, in turn, prevents even the valid user from obtaining access to their account.

This can be prevented with the usage of the rate-limiter pattern.

Rate Limiters

For an ideal amount of performance, you can run a light-weight web framework with functions for managing logs on an in-memory database for monitoring and logging the traffic data, be it based on IP or User or Service called by user. The more important choice is the data storage you want to employ.

Best and most used free options are:

  • redis.io advanced key-value store
  • ehcache standards-based cache, actively developed, maintained and supported as a professional open source project by Terracotta
  • hazelcast an open source In-Memory Data Grid for faster execution and seamless elastic scalability
  • VoltDB an in-memory operational database

Regular Expression Denial of Service (ReDoS)

The Regular expression Denial of Service (ReDoS) is a Denial of Service attack, that exploits the fact that most Regular Expression implementations may reach extreme situations that cause them to work very slowly (exponentially related to input size). An attacker can then cause a program using a Regular Expression to enter these extreme situations and then hang for a very long time.

Examples of Evil Patterns:

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

To check your own regular expressions agianst such an attack, you can use safe-regex:

node safe.js '(beep|boop)*' // true
node safe.js '(a+){10}' // false

Error Handling

Stack traces are not treated as vulnerabilities by themselves, but they often reveal information that can be interesting to an attacker. Providing debugging information as a result of operations that generate errors is considered a bad practice. You should always log them, but do not show them to the users.

HTTP Strict Transport Security (HSTS)

HSTS is an opt-in security enhancement that is specified by a web application through the use of a special response header. Once a supported browser receives this header that browser will prevent any communications from being sent over HTTP to the specified domain and will instead send all communications over HTTPS. It also prevents HTTPS click through prompts on browsers.

To test for HSTS:

curl -s -D- https://facebook.com/ | grep -i Strict  

NPM

NPM packages may contain security vulnerabilities that are critical.

You can use nsp or requireSafe to check modules.

npm i nsp -g 
nsp audit-package

Secure Transmission

As HTTP is a clear-text protocol it must be secured via SSL/TLS tunnel, known as HTTPS. Nowadays high-grade ciphers are normally used, misconfiguration in the server can be used to force the use of a weak cipher - or at worst no encryption.

You have to test:

  • certificate validity
  • ciphers, keys and renegotiation is properly configured

Checking for Certificate information - using nmap

nmap --script ssl-cert,ssl-enum-ciphers -p 443,465,993,995 www.example.com 

Testing SSL/TLS vulnerabilities with sslyze

./sslyze.py --regular example.com:3000

Session Management

Cookie Flags

The following is a list of the attributes that can be set for each cookie and what they mean:

  • 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.
  • secure this attribute tells the browser to only send the cookie if the request is being sent over HTTPS.

Creating a cookie using cookies package:

var cookieSession = require('cookie-session');
var express = require('express');

var app = express();

app.use(cookieSession({
  name: 'session',
  keys: [
    process.env.COOKIE_KEY1,
    process.env.COOKIE_KEY2
  ]
}));

app.use(function (req, res, next) {
  var n = req.session.views || 0;
  req.session.views = n++;
  res.end(n + ' views');
});

app.listen(3000);

The code snippets for this post can be found on Github.