Introduction

This cheat sheet lists the things one can use when developing secure Node.js applications. Each item has a brief explanation and solution that is specific to Node.js environment.

Context

Node.js applications are increasing in number and they are no different from other frameworks and programming languages. Node.js applications are also prone to all kinds of web application vulnerabilities.

Objective

This cheat sheet aims to provide a list of best practices to follow during development of Node.js applications.

Recommendations

There are several different recommendations to enhance security of your Node.js applications. There are categorized as:

  • Application Security
  • Error & Exception Handling
  • Server Security
  • Platform Security

Application Security

Use flat Promise chains

Asynchronous callback functions are one of the strongest features of Node.js. However, increasing layers of nesting within callback functions can become a problem. Any multistage process can become nested 10 or more levels deep. This problem is called as Pyramid of Doom or Callback Hell. In such a code, the errors and results get lost within the callback. Promises are a good way to write asynchronous code without getting into nested pyramids. Promises provide top-down execution while being asynchronous by delivering errors and results to next .then function.

Another advantage of Promises is the way Promises handle the errors. If an error occurs in a Promise class, it skips over the .then functions and invokes the first .catch function it finds. This way Promises bring a higher assurance of capturing and handling errors. As a principle, you can make all your asynchronous code (apart from emitters) return promises. However, it should be noted that Promise calls can also become a pyramid. In order to completely stay away from callback hells, flat Promise chains should be used. If the module you are using does not support Promises, you can convert base object to a Promise by using Promise.promisifyAll() function.

The following code snippet is an example of callback hell.

  1. function func1(name, callback) {
  2. setTimeout(function() {
  3. // operations
  4. }, 500);
  5. }
  6. function func2(name, callback) {
  7. setTimeout(function() {
  8. // operations
  9. }, 100);
  10. }
  11. function func3(name, callback) {
  12. setTimeout(function() {
  13. // operations
  14. }, 900);
  15. }
  16. function func4(name, callback) {
  17. setTimeout(function() {
  18. // operations
  19. }, 3000);
  20. }
  21. func1("input1", function(err, result1){
  22. if(err){
  23. // error operations
  24. }
  25. else {
  26. //some operations
  27. func2("input2", function(err, result2){
  28. if(err){
  29. //error operations
  30. }
  31. else{
  32. //some operations
  33. func3("input3", function(err, result3){
  34. if(err){
  35. //error operations
  36. }
  37. else{
  38. // some operations
  39. func4("input 4", function(err, result4){
  40. if(err){
  41. // error operations
  42. }
  43. else {
  44. // some operations
  45. }
  46. });
  47. }
  48. });
  49. }
  50. });
  51. }
  52. });

Above code can be securely written as follows using flat Promise chain:

  1. function func1(name, callback) {
  2. setTimeout(function() {
  3. // operations
  4. }, 500);
  5. }
  6. function func2(name, callback) {
  7. setTimeout(function() {
  8. // operations
  9. }, 100);
  10. }
  11. function func3(name, callback) {
  12. setTimeout(function() {
  13. // operations
  14. }, 900);
  15. }
  16. function func4(name, callback) {
  17. setTimeout(function() {
  18. // operations
  19. }, 3000);
  20. }
  21. func1("input1")
  22. .then(function (result){
  23. return func2("input2");
  24. })
  25. .then(function (result){
  26. return func3("input3");
  27. })
  28. .then(function (result){
  29. return func4("input4");
  30. })
  31. .catch(function (error) {
  32. // error operations
  33. });

Set request size limits

Buffering and parsing of request bodies can be resource intensive for the server. If there is no limit on the size of requests, attackers can send request with large request bodies so that they can exhaust server memory or fill disk space. However, fixing a request size limit for all requests may not be the correct behavior, since some requests like those for uploading a file to the server have more content to carry on the request body. Also, input with a JSON type is more dangerous than a multipart input, since parsing JSON is a blocking operation. Therefore, you should set request size limits for different content types. You can accomplish this very easily with express middlewares as follows:

  1. app.use(express.urlencoded({ limit: "1kb" }));
  2. app.use(express.json({ limit: "1kb" }));
  3. app.use(express.multipart({ limit:"10mb" }));
  4. app.use(express.limit("5kb")); // this will be valid for every other content type

However, it should be noted that attackers can change content type of the request and bypass request size limits. Therefore, before processing the request, data contained in the request should be validated against the content type stated in the request headers. If content type validation for each request affects the performance severely, you can only validate specific content types or request larger than a predetermined size.

Do not block the event loop

Node.js is very different from common application platforms that use threads. Node.js has a single-thread event-driven architecture. By means of this architecture, throughput becomes high and programming model becomes simpler. Node.js is implemented around a non-blocking I/O event loop. With this event loop, there is no waiting on I/O or context switching. The event loop looks for events and dispatches them to handler functions. Because of this, when CPU intensive JavaScript operations are done, the event loop waits for them to finish. This is why such operations are called blocking. To overcome this problem, Node.js allows assigning callbacks to IO-blocked events. This way, the main application is not blocked and callbacks run asynchronously. Therefore, as a general principle, all blocking operations should be done asynchronously so that the event loop is not blocked.

Even if you perform blocking operations asynchronously, it is still possible that your application may not serve as expected. This happens if there is a code outside the callback which relies on the code within the callback to run first. For example, consider the following code:

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

In the above example, unlinkSync function may run before the callback, which will delete the file before the desired actions on the file content is done. Such race conditions can also impact the security of your application. An example would be a scenario where authentication is performed in callback and authenticated actions are run synchronously. In order to eliminate such race conditions, you can write all operations that rely on each other in a single non-blocking function. By doing so, you can guarantee that all operations are executed in the correct order. For example, above code example can be written in a non-blocking way as follows:

  1. const fs = require('fs');
  2. fs.readFile('/file.txt', (err, data) => {
  3. // perform actions on file content
  4. fs.unlink('/file.txt', (err) => {
  5. if (err) throw err;
  6. });
  7. });

In the above code, call to unlink the file and other file operations are within the same callback. This provides the correct order of operations.

Perform input validation

Input validation is a crucial part of application security. Input validation failures can result in many different types of application attacks. These include SQL Injection, Cross-Site Scripting, Command Injection, Local/Remote File Inclusion, Denial of Service, Directory Traversal, LDAP Injection and many other injection attacks. In order to avoid these attacks, input to your application should be sanitized first. The best input validation technique is to use a white list of accepted inputs. However, if this is not possible, input should be first checked against expected input scheme and dangerous inputs should be escaped. In order to ease input validation in Node.js applications, there are some modules like validator and mongo-express-sanitize.
For detailed information on input validation, please refer to Input Validation Cheat Sheet.

Perform output escaping

In addition to input validation, you should escape all HTML and JavaScript content shown to users via application in order to prevent cross-site scripting (XSS) attacks. You can use escape-html or node-esapi libraries to perform output escaping.

Perform application activity logging

Logging application activity is an encouraged good practice. It makes it easier to debug any errors encountered during application runtime. It is also useful for security concerns, since it can be used during incident response. Also, these logs can be used to feed Intrusion Detection/Prevention Systems (IDS/IPS). In Node.js, there are some modules like Winston or Bunyan to perform application activity logging. These modules enable streaming and querying logs. Also, they provide a way to handle uncaught exceptions. With the following code, you can log application activities in both console and a desired log file.

  1. var logger = new (Winston.Logger) ({
  2. transports: [
  3. new (winston.transports.Console)(),
  4. new (winston.transports.File)({ filename: 'application.log' })
  5. ],
  6. level: 'verbose'
  7. });

Also, you can provide different transports so that you can save errors to a separate log file and general application logs to a different log file. Additional information on security logging can be found in Logging Cheat Sheet.

Monitor the event loop

When your application server is under heavy network traffic, it may not be able to serve its users. This is essentially a type of Denial of Service (DoS) attack. The toobusy-js module allows you to monitor the event loop. It keeps track of the response time, and when it goes beyond a certain threshold, this module can indicate your server is too busy. In that case, you can stop processing incoming requests and send them 503 Server Too Busy message so that your application stay responsive. Example use of the toobusy-js module is shown here:

  1. var toobusy = require('toobusy-js');
  2. var express = require('express');
  3. var app = express();
  4. app.use(function(req, res, next) {
  5. if (toobusy()) {
  6. // log if you see necessary
  7. res.send(503, "Server Too Busy");
  8. } else {
  9. next();
  10. }
  11. });

Take precautions against brute-forcing

Brute-forcing is a common threat to all web applications. Attackers can use brute-forcing as a password guessing attack to obtain account passwords. Therefore, application developers should take precautions against brute-force attacks especially in login pages. Node.js has several modules available for this purpose. Express-bouncer, express-brute and rate-limiter are just some examples. Based on your needs and requirements, you should choose one or more of these modules and use accordingly. Express-bouncer and express-brute modules work very similar and they both increase the delay with each failed request. They can both be arranged for a specific route. These modules can be used as follows:

  1. var bouncer = require('express-bouncer');
  2. bouncer.whitelist.push('127.0.0.1'); // whitelist an IP address
  3. // give a custom error message
  4. bouncer.blocked = function (req, res, next, remaining) {
  5. res.send(429, "Too many requests have been made. Please wait " + remaining/1000 + " seconds.");
  6. };
  7. // route to protect
  8. app.post("/login", bouncer.block, function(req, res) {
  9. if (LoginFailed){ }
  10. else {
  11. bouncer.reset( req );
  12. }
  13. });
  1. var ExpressBrute = require('express-brute');
  2. var store = new ExpressBrute.MemoryStore(); // stores state locally, don't use this in production
  3. var bruteforce = new ExpressBrute(store);
  4. app.post('/auth',
  5. bruteforce.prevent, // error 429 if we hit this route too often
  6. function (req, res, next) {
  7. res.send('Success!');
  8. }
  9. );

Apart from express-bouncer and express-brute, rate-limiter module also helps prevent brute-forcing attacks. It enables specifying how many requests a specific IP address can make during a specified time period.

  1. var limiter = new RateLimiter();
  2. limiter.addLimit('/login', 'GET', 5, 500); // login page can be requested 5 times at max within 500 seconds

CAPTCHA usage is also another common mechanism used against brute-forcing. There are modules developed for Node.js CAPTCHAs. A common module used in Node.js applications is svg-captcha. It can be used as follows:

  1. var svgCaptcha = require('svg-captcha');
  2. app.get('/captcha', function (req, res) {
  3. var captcha = svgCaptcha.create();
  4. req.session.captcha = captcha.text;
  5. res.type('svg');
  6. res.status(200).send(captcha.data);
  7. });

Also, account lockout is a recommended solution to keep attackers away from your valid users. Account lockout is possible with many modules like mongoose. You can refer to this blog post to see how account lockout is implemented in mongoose.

Use Anti-CSRF tokens

Cross-Site Request Forgery (CSRF) aims to perform authorized actions on behalf of an authenticated user, while the user is unaware of this action. CSRF attacks are generally performed for state-changing requests like changing a password, adding users or placing orders. Csurf is an express middleware that can be used to mitigate CSRF attacks. It can be used as follows:

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

After writing this code, you also need to add csrfToken to your HTML form, which can be easily done as follows:

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

For detailed information on cross-site request forgery (CSRF) attacks and prevention methods, you can refer to Cross-Site Request Forgery Prevention.

Remove unnecessary routes

A web application should not contain any page that is not used by users, as it may increase the attack surface of the application. Therefore, all unused API routes should be disabled in Node.js applications. This occurs especially in frameworks like Sails and Feathers, as they automatically generate REST API endpoints. For example, in Sails, if a URL does not match a custom route, it may match one of the automatic routes and still generate a response. This situation may lead to results ranging from information leakage to arbitrary command execution. Therefore, before using such frameworks and modules, it is important to know the routes they automatically generate and remove or disable these routes.

Prevent HTTP Parameter Pollution

HTTP Parameter Pollution(HPP)) is an attack in which attackers send multiple HTTP parameters with the same name and this causes your application to interpret them in an unpredictable way. When multiple parameter values are sent, Express populates them in an array. In order to solve this issue, you can use hpp module. When used, this module will ignore all values submitted for a parameter in req.query and/or req.body and just select the last parameter value submitted. You can use it as follows:

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

Only return what is necessary

Information about the users of an application is among the most critical information about the application. User tables generally include fields like id, username, full name, email address, birth date, password and in some cases social security numbers. Therefore, when querying and using user objects, you need to return only needed fields as it may be vulnerable to personal information disclosure. This is also correct for other objects stored on the database. If you just need a certain field of an object, you should only return the specific fields required. As an example, you can use a function like the following whenever you need to get information on a user. By doing so, you can only return the fields that are needed for your specific operation. In other words, if you only need to list names of the users available, you are not returning their email addresses or credit card numbers in addition to their full names.

  1. exports.sanitizeUser = function(user) {
  2. return {
  3. id: user.id,
  4. username: user.username,
  5. fullName: user.fullName
  6. };
  7. };

Use object property descriptors

Object properties include 3 hidden attributes: writable (if false, property value cannot be changed), enumerable (if false, property cannot be used in for loops) and configurable (if false, property cannot be deleted). When defining an object property through assignment, these three hidden attributes are set to true by default. These properties can be set as follows:

  1. var o = {};
  2. Object.defineProperty(o, "a", {
  3. writable: true,
  4. enumerable: true,
  5. configurable: true,
  6. value: "A"
  7. });

Apart from these, there are some special functions for object attributes. Object.preventExtensions() prevents new properties from being added to the object.

Use access control lists

Authorization prevents users from acting outside of their intended permissions. In order to do so, users and their roles should be determined with consideration of the principle of least privilege. Each user role should only have access to the resources they must use. For your Node.js applications, you can use acl module to provide ACL (access control list) implementation. With this module, you can create roles and assign users to these roles.

Error & Exception Handling

Handle uncaughtException

Node.js behavior for uncaught exceptions is to print current stack trace and then terminate the thread. However, Node.js allows customization of this behavior. It provides a global object named process which is available to all Node.js applications. It is an EventEmitter object and in case of an uncaught exception, uncaughtException event gets emitted and it is brought up to the main event loop. In order to provide a custom behavior for uncaught exceptions, you can bind to this event. However, resuming the application after such an uncaught exception can lead to further problems. Therefore, if you do not want to miss any uncaught exception, you should bind to uncaughtException event and cleanup any allocated resources like file descriptors, handles and similar before shutting down the process. Resuming the application is strongly discouraged as the application will be in an unknown state. Also, it is important to note that when displaying error messages to the user in case of an uncaught exception, detailed information like stack traces should not be revealed to the user. Instead, custom error messages should be shown to the users in order not to cause any information leakage.

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

Listen to errors when using EventEmitter

When using EventEmitter, errors can occur anywhere in the event chain. Normally, if an error occurs in an EventEmitter object, an error event which has an Error object as an argument is called. However, if there are no attached listeners to that error event, the Error object that is sent as an argument is thrown and becomes an uncaught exception. In short, if you do not handle errors within an EventEmitter object properly, these unhandled errors may crash your application. Therefore, you should always listen to error events when using EventEmitter objects.

  1. var events = require('events');
  2. var myEventEmitter = function(){
  3. events.EventEmitter.call(this);
  4. }
  5. require('util').inherits(myEventEmitter, events.EventEmitter);
  6. myEventEmitter.prototype.someFunction = function(param1, param2) {
  7. //in case of an error
  8. this.emit('error', err);
  9. }
  10. var emitter = new myEventEmitter();
  11. emitter.on('error', function(err){
  12. //Perform necessary error handling here
  13. });

Handle errors in asynchronous calls

Errors that occur within asynchronous callbacks are easy to miss. Therefore, as a general principle first argument to the asynchronous calls should be an Error object. Also, express routes handle errors itself, but it should be always remembered that errors occurred in asynchronous calls made within express routes are not handled, unless an Error object is sent as a first argument.

Errors in these callbacks can be propagated as many times as possible. Each callback that the error has been propagated to can ignore, handle or propagate the error.

Server Security

Set cookie flags appropriately

Generally, session information is sent using cookies in web applications. However, improper use of HTTP cookies can render an application to several session management vulnerabilities. There are some flags that can be set for each cookie to prevent these kinds of attacks. httpOnly, Secure and SameSite flags are very important for session cookies. httpOnly flag prevents the cookie from being accessed by client-side JavaScript. This is an effective counter-measure for XSS attacks. Secure flag lets the cookie to be sent only if the communication is over HTTPS. SameSite flag can prevent cookies from being sent in cross-site requests which helps protect against Cross-Site Request Forgery (CSRF) attacks. Apart from these, there are other flags like domain, path and expires. Setting these flags appropriately is encouraged, but they are mostly related to cookie scope not the cookie security. Sample usage of these flags is given in the following example:

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

Use appropriate security headers

There are several different HTTP security headers that can help you prevent some common attack vectors. These are listed below:

  1. app.use(helmet.hsts()); // default configuration
  2. app.use(helmet.hsts("<max-age>", "<includeSubdomains>")); // custom configuration
  • X-Frame-Options: determines if a page can be loaded via a \ or an \