Security Cheat Sheets: Nodejs



Quite a lot has already been said about the popularity of NodeJS. The increase in the number of applications is obvious - NodeJS is quite easy to learn, has a huge number of libraries, as well as a dynamically developing ecosystem.

We made recommendations for NodeJS developers based on OWASP Cheat Sheets to help you anticipate security issues when developing applications.

Security recommendations for NodeJS applications can be divided into the following categories:

  • Security during application development;
  • Server security;
  • Platform security;


Application Development Security


Avoid callback hell

Using callback functions (callbacks) is one of NodeJS's greatest strengths, however when nesting callbacks, you can easily forget to handle the error in one of the functions. One way to avoid callback hell is to use promises. Even if the module you are using does not support working with promises, you can always use Promise.promisifyAll (). But even using promises, it is worth paying attention to nesting. To completely avoid the callback hell error, stick to a β€œflat” chain of promises.

Callback hell example:

function func1(name, callback) {
   setTimeout(function() {
      // operations
   }, 500);
}
function func2(name, callback) {
   setTimeout(function() {
      // operations
   }, 100);
}
function func3(name, callback) {
   setTimeout(function() {
      // operations
   }, 900);
}
function func4(name, callback) {
   setTimeout(function() {
      // operations
   }, 3000);
}

func1("input1", function(err, result1){
   if(err){
      // error operations
   }
   else {
      //some operations
      func2("input2", function(err, result2){
         if(err){
            //error operations
         }
         else{
            //some operations
            func3("input3", function(err, result3){
               if(err){
                  //error operations
               }
               else{
                  // some operations
                  func4("input 4", function(err, result4){
                     if(err){
                        // error operations
                     }
                     else {
                        // some operations
                     }
                  });
               }
            });
         }
      });
   }
});

Same code using flat chain promises:

function func1(name, callback) {
   setTimeout(function() {
      // operations
   }, 500);
}
function func2(name, callback) {
   setTimeout(function() {
      // operations
   }, 100);
}
function func3(name, callback) {
   setTimeout(function() {
      // operations
   }, 900);
}
function func4(name, callback) {
   setTimeout(function() {
      // operations
   }, 3000);
}

func1("input1")
   .then(function (result){
      return func2("input2");
   })
   .then(function (result){
      return func3("input3");
   })
   .then(function (result){
      return func4("input4");
   })
   .catch(function (error) {
      // error operations
   });

Limit the size of the request.

Parsing the request body can be quite a resource-intensive operation. If you do not limit the size of the request, attackers will be able to send large enough requests that can fill up all disk space or exhaust all server resources, but at the same time, limiting the request size for all cases may be incorrect, because there are requests, such as downloading a file. Therefore, it is recommended to set limits for different types of content. For example, using the express framework, this can be implemented as follows:

app.use(express.urlencoded({ limit: "1kb" }));
app.use(express.json({ limit: "1kb" }));
app.use(express.multipart({ limit:"10mb" }));

It should be noted that an attacker can change the type of request content and circumvent restrictions, therefore, it is necessary to check whether the content of the request matches the type of content specified in the request header. If checking the type of content affects performance, you can only check certain types or queries that are larger than a certain size.

Do not block the event loop

An important component of the language is the event loop, which just allows you to switch the execution context without waiting for the operation to complete. However, there are blocking operations whose completion NodeJS has to wait before continuing with the code. For example, most synchronous methods are blocking:

const fs = require('fs');
fs.unlinkSync('/file.txt');

It is recommended to perform such operations asynchronously:

const fs = require('fs');
fs.unlink('/file.txt', (err) => {
    if (err) throw err;
});

At the same time, do not forget that the code standing after the asynchronous call will be executed without waiting for the completion of the previous operation.

For example, in the code below, the file will be deleted before it is read, which can lead to a race condition.

const fs = require('fs');
fs.readFile('/file.txt', (err, data) => {
  // perform actions on file content
});
fs.unlinkSync('/file.txt');

To avoid this, you can write all operations in a non-blocking function:

const fs = require('fs');
fs.readFile('/file.txt', (err, data) => {
  // perform actions on file content
  fs.unlink('/file.txt', (err) => {
    if (err) throw err;
  });
});

Check input fields.

Checking input fields is an important part of the security of any application. Validation errors can cause your application to become vulnerable immediately to many types of attacks: sql injection, xss, command injection, and others. To simplify form validation, you can use validator packages, mongo-express-sanitize.

Escape user data

One of the rules that will help you protect yourself from xss attacks is to shield user data. You can use the escape-html or node-esapi library for this.

Keep logs

In addition, it will help in debugging errors, logging can be used to respond to incidents. You can read more about the need for logging here.. One of the most popular NodeJS logging packages is Winston and Bunyan. The example below shows how to use Winston to output logs to both the console and the file:

var logger = new (Winston.Logger) ({
    transports: [
        new (winston.transports.Console)(),
        new (winston.transports.File)({ filename: 'application.log' })
    ],
    level: 'verbose'
});

Control the cycle of events

If your server is in conditions of intensive network traffic, users may experience difficulties with the availability of your service. This is essentially a DoS attack. In this case, you can track the response time and, if it exceeds the specified time, send a message to 503 Server Too Busy. The toobusy-js module can help.

An example of using the module:

var toobusy = require('toobusy-js');
var express = require('express');
var app = express();
app.use(function(req, res, next) {
    if (toobusy()) {
        // log if you see necessary
        res.send(503, "Server Too Busy");
    } else {
    next();
    }
});

Take precautions against brute force.

Again, modules come to the rescue. For example, express-brute or express-bouncer. Usage example:

var bouncer = require('express-bouncer');
bouncer.whitelist.push('127.0.0.1'); // whitelist an IP address
// give a custom error message
bouncer.blocked = function (req, res, next, remaining) {
    res.send(429, "Too many requests have been made. Please wait " + remaining/1000 + " seconds.");
};
// route to protect
app.post("/login", bouncer.block, function(req, res) {
    if (LoginFailed){  }
    else {
        bouncer.reset( req );
    }
});

Using CAPTCHA is another common brute force countermeasure. A frequently used module to help implement CAPTCHA is svg-captcha.

Use CSRF Tokens

One of the most reliable ways to protect against CSRF attacks is to use a CSRF token. The token must be generated with high entropy, strictly checked and be tied to the user's session. To ensure the operation of the CSRF token, you can use the csurf module.

Usage example:

var csrf = require('csurf');
csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, function(req, res) {
    res.render('send', { csrfToken: req.csrfToken() })
})
app.post('/process', parseForm, csrfProtection, function(req, res) {
    res.send('data is being processed');
});

Do not forget to add the token to the hidden field on the page:

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

You can read more about CSRF tokens in our article .

Delete unnecessary routes. The

web application should not contain pages that are not used by users, as this can increase the attack surface. Therefore, all unused API routes must be disabled. You should especially pay attention to this question if you use Sails or Feathers frameworks, since they automatically generate API endpoints.

Protect yourself from HPP (HTTP Parameter Pollution)

By default, express adds all the parameters from the request to an array. OWASP recommends using the hpp module, which ignores all parameter values ​​from req.query and / or req.body and simply selects the last value among the duplicate ones.

var hpp = require('hpp');
app.use(hpp());

Monitor the returned values.

For example, the user table can store important data: password, email address, date of birth, etc. Therefore, it is important to return only the necessary data.

For instance:

 exports.sanitizeUser = function(user) {
  return {
    id: user.id,
    username: user.username,
    fullName: user.fullName
  };
};

Use descriptors

Use descriptors to describe the behavior of a property for various operations: writable - whether it is possible to change the value of a property, enumerable - whether it is possible to use a property in a for..in loop, configurable - whether it is possible to overwrite a property. It is recommended to pay attention to the listed properties, since when defining the property of an object, all these attributes are set to true by default. You can change the value of properties as follows:

var o = {};
Object.defineProperty(o, "a", {
    writable: true,
    enumerable: true,
    configurable: true,
    value: "A"
});

Use ACLs

Acl can help to differentiate data access based on roles. For example, adding permission looks like this:

// guest is allowed to view blogs
acl.allow('guest', 'blogs', 'view')
// allow function accepts arrays as any parameter
acl.allow('member', 'blogs', ['edit', 'view', 'delete'])

Catch uncaughtException

By default, in the event of an uncaught exception, NodeJS will throw the current stack trace and terminate the execution thread. However, NodeJS allows you to customize this behavior. In the event of an uncaught exception, an uncaughtException event is raised, which can be caught using the process object:

process.on("uncaughtException", function(err) {
    // clean up allocated resources
    // log necessary error details to log files
    process.exit(); // exit the process to avoid unknown state
});

It is worth remembering that when an uncaughtException occurs, it is necessary to clear all allocated resources (for example, file descriptors and handlers) before completing the Z process in order to avoid unforeseen errors. It is strongly discouraged that the program continues to run if an uncaughtException occurs.

Also, when displaying error messages, the user should not disclose detailed error information, such as stack trace.

Server security


Set flags for headers when working with cookies.

There are several flags that can help protect against attacks such as xss and csrf: httpOnly, which prevents access to cookies through javascript; Secure - allows sending cookies only via HTTPS and SameSite, which determines the ability to transfer cookies to a third-party resource.

Usage example:

var session = require('express-session');
app.use(session({
    secret: 'your-secret-key',
    key: 'cookieName',
    cookie: { secure: true, httpOnly: true, path: '/user', sameSite: true}
}));

Set HTTP headers for security

The following are headers and examples of how to connect them to help you protect yourself from a number of common attacks. Headers are set using the helmet module

β€’ Strict-Transport-Security: HTTP Strict Transport Security (HSTS) tells the browser that the application can only be accessed via HTTPS

app.use(helmet.hsts()); // default configuration
app.use(helmet.hsts("<max-age>", "<includeSubdomains>")); // custom configuration

β€’ X-Frame-Options: determines whether the page can be used in frame, iframe, embed or object

app.use(hemlet.xframe()); // default behavior (DENY)
helmet.xframe('sameorigin'); // SAMEORIGIN
helmet.xframe('allow-from', 'http://alloweduri.com'); //ALLOW-FROM uri

β€’ X-XSS-Protection: allows the browser to stop loading the page if it detects a reflected XSS attack.

var xssFilter = require('x-xss-protection');
app.use(xssFilter());

β€’ X-Content-Type-Options: used to prevent attacks using MIME types

app.use(helmet.noSniff());

β€’ Content-Security-Policy: Prevents attacks such as XSS and data injection attacks

const csp = require('helmet-csp')
app.use(csp({
   directives: {
       defaultSrc: ["'self'"],  // default value for all directives that are absent
       scriptSrc: ["'self'"],   // helps prevent XSS attacks
       frameAncestors: ["'none'"],  // helps prevent Clickjacking attacks
       imgSrc: ["'self'", "'http://imgexample.com'"],
       styleSrc: ["'none'"]
    }
}))

β€’ Cache-Control and Pragma: for managing caching, especially this header can be useful for pages that contain sensitive data. However, remember that disabling caching on all pages can affect performance.

app.use(helmet.noCache());

β€’ X-Download-Options: header prevents Inter Explorer from executing downloaded files

app.use(helmet.ieNoOpen());

β€’ Expect-CT: Certificate Transparency - a mechanism created to solve some problems with the infrastructure of SSL certificates, this header tells the browser about the need for additional certificate verification in CT logs

var expectCt = require('expect-ct');
app.use(expectCt({ maxAge: 123 }));
app.use(expectCt({ enforce: true, maxAge: 123 }));
app.use(expectCt({ enforce: true, maxAge: 123, reportUri: 'http://example.com'}));

β€’ X-Powered-By: An optional header that is used to indicate the technology used on the server. You can hide this header as follows:

app.use(helmet.hidePoweredBy());

In addition, you can change the value to hide real information about the technologies you use:

app.use(helmet.hidePoweredBy({ setTo: 'PHP 4.2.0' }));

Platform security


Update your packages The

security of your application depends on the security of the packages you use, so it is important to use the latest version of the package. To make sure that the package you are using does not contain known vulnerabilities, you can use the special OWASP list . You can also use the library that checks packages for known vulnerabilities Retire.js.

Do not use unsafe functions.

There are functions that are recommended to be discarded whenever possible. Among these functions is eval (), which executes a string taken as an argument. In combination with user input, using this function can lead to vulnerabilities in remote code execution, since for similar reasons, using child_process.exec is also unsafe, because the function passes the received arguments to bin / sh.

In addition, there are a number of modules that you should use with caution. For example, the fs module for working with files. If in a certain way the generated user input is passed into a function, then your application may become vulnerable to including a local file and directory traversal.

The vm module, which provides an API for compiling and running code on a V8 virtual machine, should be used only in the sandbox.

Here you can familiarize yourself with other functions that may make your application unsafe.

Be careful using regular expressions.

A regular expression can be written so that you can achieve a situation where the expression will grow exponentially, which can lead to a denial of service. Such attacks are called ReDoS. There are several tools to check if regular expressions are safe, one of which is vuln-regex-detector.

Run linter periodically

During development, it’s difficult to keep in mind all the recommendations for ensuring security, and if it comes to team development, it is not easy to achieve compliance with the rules by all team members. For such purposes, there are tools for static security analysis. Such tools, without executing your code, look for vulnerabilities in it. In addition, linters allow you to add custom rules for finding places in the code that may be vulnerable. The most commonly used linters are ESLint and JSHint.

Use strict mode.

Javascript has a number of insecure and obsolete functions that should not be used. To exclude the possibility of using these functions, strict mode is also provided.

Adhere to general safety principles

The recommendations described focus on NodeJS, but do not forget about the general security principles that must be followed regardless of the platform used.

All Articles