Patterns in Node Package Vulnerabilities

Essential Findings for Busy Developers

Chetan Karande

Patterns in Node Package Vulnerabilities

by Chetan Karande

Copyright © 2018 O’Reilly Media, Inc. All rights reserved.

Printed in the United States of America.

Published by O’Reilly Media, Inc., 1005 Gravenstein Highway North, Sebastopol, CA 95472.

O’Reilly books may be purchased for educational, business, or sales promotional use. Online editions are also available for most titles (http://oreilly.com/safari). For more information, contact our corporate/institutional sales department: 800-998-9938 or corporate@oreilly.com.

Editor: Allyson MacDonald

Production Editor: Nan Barber

Copyeditor: Octal Publishing, Inc. 

Interior Designer: David Futato

Cover Designer: Karen Montgomery

Illustrator: Rebecca Demarest

Technical reviewers: Bryan Devereaux, Sonia Burney

June 2018: First Edition

Revision History for the First Edition

2018-06-01: First Release

The O’Reilly logo is a registered trademark of O’Reilly Media, Inc. Patterns in Node Package Vulnerabilities, the cover image, and related trade dress are trademarks of O’Reilly Media, Inc.

The views expressed in this work are those of the author, and do not represent the publisher’s views. While the publisher and the author have used good faith efforts to ensure that the information and instructions contained in this work are accurate, the publisher and the author disclaim all responsibility for errors or omissions, including without limitation responsibility for damages resulting from the use of or reliance on this work. Use of the information and instructions contained in this work is at your own risk. If any code samples or other technology this work contains or describes is subject to open source licenses or the intellectual property rights of others, it is your responsibility to ensure that your use thereof complies with such licenses and/or rights.

978-1-491-99997-4

[LSI]

Preface

This report is primarily targeted for Node.js developers. It provides distilled findings from the analysis of known security vulnerabilities in packages published on the npm public registry.

A recent survey reveals that security is one of the top concerns for Node.js developers, but it is also the least-explained topic. In this regard, the lessons learned from frequently recurring security mistakes in more than a thousand publicly published Node package vulnerabilities could be a great learning resource. These lessons are helpful not only to package authors but equally useful to Node.js application developers and security testers. However, this valuable information is mostly untapped: with a mere chronological arrangement of the data in the vulnerabilities databases, it is often difficult to make necessary associations and infer actionable lessons from related coding flaws. To fill in this gap, based on painstaking analysis and data crunching from leading Snyk and npm Node.js advisories databases, this report presents actionable findings. Chapter 1 brings forth the patterns found in known vulnerabilities, including classification by attack types, severity, and frequency of occurrence. Chapter 2 further builds upon this analysis, highlighting common coding mistakes behind the top five vulnerability types and offers practical advice on avoiding them.

The information gained will help readers avoid common security issues when developing their own Node.js packages and applications; vetting external dependencies before using them; and identifying possible security vulnerabilities when conducting security-focused code reviews and penetration testing.

Conventions Used in This Book

The following typographical conventions are used in this book:

Italic

Indicates new terms, URLs, email addresses, filenames, and file extensions.

Constant width

Used for program listings, as well as within paragraphs to refer to program elements such as variable or function names, databases, data types, environment variables, statements, and keywords.

Constant width bold

Shows commands or other text that should be typed literally by the user.

Constant width italic

Shows text that should be replaced with user-supplied values or by values determined by context.

Tip

This element signifies a tip or suggestion.

Note

This element signifies a general note.

Warning

This element indicates a warning or caution.

O’Reilly Safari

Note

Safari (formerly Safari Books Online) is a membership-based training and reference platform for enterprise, government, educators, and individuals.

Members have access to thousands of books, training videos, Learning Paths, interactive tutorials, and curated playlists from over 250 publishers, including O’Reilly Media, Harvard Business Review, Prentice Hall Professional, Addison-Wesley Professional, Microsoft Press, Sams, Que, Peachpit Press, Adobe, Focal Press, Cisco Press, John Wiley & Sons, Syngress, Morgan Kaufmann, IBM Redbooks, Packt, Adobe Press, FT Press, Apress, Manning, New Riders, McGraw-Hill, Jones & Bartlett, and Course Technology, among others.

For more information, please visit http://oreilly.com/safari.

How to Contact Us

Please address comments and questions concerning this book to the publisher:

O’Reilly Media, Inc.

1005 Gravenstein Highway North

Sebastopol, CA 95472

800-998-9938 (in the United States or Canada)

707-829-0515 (international or local)

707-829-0104 (fax)

To comment or ask technical questions about this book, send email to bookquestions@oreilly.com.

For more information about our books, courses, conferences, and news, see our website at http://www.oreilly.com.

Find us on Facebook: http://facebook.com/oreilly

Follow us on Twitter: http://twitter.com/oreillymedia

Watch us on YouTube: http://www.youtube.com/oreillymedia

Acknowledgments

I would like to express my gratitude to the passionate O’Reilly team, especially Virginia Wilson for all the support that helped make this report a reality. I would also like to thank Bryan Devereaux and Sonia Burney for the invaluable feedback that improved this report. Last but not least, countless thanks to my wonderful wife, Aparna, for keeping all the ducks (including two little ones at home) in a row so I could indulge in the world of Node vulnerabilities for many long hours.

Chapter 1. Analysis of Known Node Package Vulnerabilities

Errors, like straws, upon the surface flow; he who would search for pearls, must dive below.

John Dryden

On average, more than 550 new Node packages are added to npm every day, making it the world’s largest software registry. Node packages are the greatest strength of the Node.js ecosystem—helping developers to reuse code, deliver features quickly, and reduce development and maintenance costs.

However, Node packages have significant implications for the security posture of Node applications. Similar to a chain that is only as strong as its weakest link, any vulnerabilities in an application’s direct or indirect Node package dependencies can make the application fall prey to security attacks.

On the bright side, the Node.js community and security researchers are publishing the Node package vulnerabilities as they are discovered. Developers also have tools that help to scan project code to detect insecure dependencies. In this chapter, we examine whether these efforts are sufficient to keep Node.js applications safe from insecure dependencies and how to prioritize security measures to achieve the maximum impact.

Current State of Security Mechanisms Against Vulnerable Dependencies

Figure 1-1 shows the rate of discovering Node package vulnerabilities by year.

Figure 1-1. Rate of discovering Node package vulnerabilities (source: Snyk and npm/Node Security Platform advisories database)

As we can see in Figure 1-1, since 2013, when Node.js became truly popular, publicly disclosed Node.js package vulnerabilities—also known as advisories—have increased at an exponential rate every year. As of April 2018, more than 1,600 Node package vulnerabilities have been reported in npm Advisories, the Snyk Vulnerability Database, and HackerOne Disclosures, combined. This is a healthy sign, demonstrating that the Node.js community is collectively assisting package authors to identify and remove security flaws in their packages.

Looking at the vulnerabilities data from another perspective, classification of known vulnerabilities by severity type shows their distribution, as illustrated in Figure 1-2. In this chart, the severity levels Critical, High, Medium, and Low reflect the Common Vulnerability Scoring System (CVSS) score greater than or equal to 9, 7, 4, and 0, respectively.

Common Vulnerability Scoring System

The CVSS is a standard that provides a way to translate severity characteristics of a vulnerability into a numerical score or a qualitative value (such as Low, Medium, High, and Critical) to help organizations properly assess and prioritize mitigations for them. For more details about factors considered for scoring, refer to the CVSS Calculator.

Figure 1-2. Distribution of vulnerabilities by severity (source: 1,023 unique advisories published by Snyk and npm/Node Security Platform as of April 2018)

As Figure 1-2 illustrates, the majority of the known vulnerabilities (62%) fall in the Critical and High-severity categories. If these went undiscovered, vulnerabilities of these severity levels could have had a catastrophic adverse effect on the availability, integrity, and data confidentiality of Node applications using insecure packages.

Furthermore, the automated dependency scanning tools such as Snyk and npm-audit that check project dependencies against known vulnerabilities make it possible to reliably keep a watch on publicly disclosed vulnerabilities and identify insecure project dependencies.

Does this mean that protecting projects against insecure Node package dependencies is a solved problem? Do we now have enough underlying advisory data and mature dependency scanning tools to assure protection against security issues in external project dependencies?

As per a survey of more than 16,000 developers jointly conducted by npm, the Node.js Foundation, and the JS Foundation, 77% of respondents express concerns with the security of the open source code they use in their projects, and more than half of JavaScript developers think that the tools they have for evaluating the security and quality of open source code aren’t good enough.

Interestingly, the vulnerability data analyzed for this report supports and is in line with the sentiments represented in the aforementioned survey.

Pace of Discovering Vulnerabilities versus Rate of Packages Released

Even though we see a healthy growth in vulnerabilities reported year by year, the rate of discovering the vulnerabilities is very slow when compared to the furious pace of new packages and their versions appearing on the npm registry.

Figure 1-3 highlights this gap by plotting the number of Node packages published against the number of vulnerabilities discovered per year.

Figure 1-3. Rate of discovering Node package vulnerabilities versus the number of Node packages on npm (source: Snyk and npm/Node Security Platform Advisories database)

Figure 1-3 poses some intriguing questions. How do we know that the number of vulnerabilities discovered in npm packages so far is too small? If so, what should be the optimal number of vulnerabilities found for the current size of the npm registry? Although there is no direct way to answer these questions—given that many factors such as expertise of the programmer, complexity of the logic, and functionalities provided by the package—can influence the number of exploitable security flaws found in a package, we can try to examine the statistics with different viewpoints to get an idea.

One way to gauge the size of npm package vulnerabilities is to compare it against related statistics from other, possibly well-tested projects.

Figure 1-4 shows the comparison of exploitable security vulnerabilities (CVSS score ≥7) discovered per Thousand Lines of Code (KLOC) in Chrome, Firefox, and all packages on the npm registry.

Here are more details on the data used for plotting the chart in Figure 1-4.

Chrome: 0.04 vulnerabilities/KLOC, based on 747 vulnerabilities found in 18,824,671 lines of code.

Firefox: 0.01 vulnerabilities/KLOC, based on 697 vulnerabilities found in 36,890,150 lines of code.

npm packages: An estimated rate of 0.0007 vulnerabilities/KLOC, based on 634 unique exploitable vulnerabilities discovered in 623,318 npm packages; and an assumption that average npm package size is 1,403 lines of code, derived from the average lines of code in the top 15 most depended upon Node packages.

Figure 1-4. Estimated vulnerabilities/KLOC rate in npm packages versus the vulnerabilities/KLOC rate in Chrome and Firefox code base

Even though the chart in Figure 1-4 is based on an assumption about average npm package size, uses programs written in different programming languages for a comparison, and doesn’t account for multiple versions of a package on the npm registry, it can help us in putting the number of npm package vulnerabilities in context to understand how few they are.

For another viewpoint, the ratio of the number of known Node package vulnerabilities to packages published on npm is close to 1:600. In other words, the Node community has found only one vulnerability in every 600 packages published on the npm registry. To put this huge gap in perspective (Figure 1-5), this ratio is around six times larger than the ratio of the Earth’s diameter to the Sun’s (1:109).

Although you can interpret this shortfall in the number of vulnerabilities discovered in favor of npm packages, considering they are inherently secure; the most likely reason for this gap is the speed of new Node packages appearing on the npm registry predominantly outpacing the limited capacity of security researchers to discover and responsibly publish security advisories.

Figure 1-5. Earth’s size as compared to that of the Sun (sources: Lsmpascal CC BY)

Path to Secure Against Vulnerable Dependencies: Gray but Not Gloomy

So what do all these statistics mean to Node developers? Here are some key takeaways:

Considering the small number of advisories when compared to the total number of packages and their versions on the npm registry, there is a chance that a package upon which an application depends never underwent a security review, which is a huge risk. As an application developer, you should conduct security reviews of the third-party packages that the application uses. Similarly, as a package author, you should perform a security review of the package code and ensure that it brings in only safe dependencies. This would have a direct impact on improving the security of every application that depends on the package.

Automating the process to keep watch on package vulnerabilities and promptly addressing identified issues are the easiest steps toward protecting against insecure dependencies. As part of the build and continuous integration process, incorporate automated tools such as Snyk CLI and npm-audit to discover vulnerable packages.

Even though using automated tools to discover vulnerable packages is indispensable, it is insufficient to reliably detect insecure dependencies. These tools use the publicly known vulnerabilities database to validate project dependencies, and current advisories data is not extensive enough. Hence, it is essential to combine automated tools with a manual code review of the packages before using them in an application.

In the next section, we take a look at the classification of known vulnerabilities by attack types. This information would assist in maximizing efforts of a manual code review.

Learning from Known Node Package Vulnerabilities

Figure 1-6 shows the distribution of Node package vulnerabilities by type.

Figure 1-6. Distribution of vulnerabilities by type (source: 1,023 unique advisories published by Snyk and npm/Node Security Platform as of April 2018)

Figure 1-6 reveals a very interesting pattern. Around 87% of known vulnerabilities fall under just five types: insecure access to the file system, sensitive data exposure, denial of service, cross-site scripting, and injection attacks. These statistics are instrumental in prioritizing efforts when writing secure code, conducting security-focused code reviews, or penetration testing.

In the next chapter, we dive deeper into these top five vulnerability types and draw actionable lessons from coding flaws associated with them.

Conclusion

In this chapter, we went over statistics revealing that although the Node community is improving its ability to discover and disclose npm package vulnerabilities as well as producing tools to notify application developers about insecure project dependencies, the speed of packages released on the npm registry is outpacing these efforts. Thus, it is essential to combine manual code review of the packages before using them. We identified patterns in existing known vulnerabilities that highlight areas to prioritize when conducting a manual review and common pitfalls to watch for while writing secure Node.js code.

Chapter 2. Common Coding Mistakes Behind Top Vulnerabilities

By seeking and blundering we learn.

Johann Wolfgang von Goethe

The advisories published in npm Advisories, Snyk Vulnerability Database, and HackerOne Disclosures are a very useful learning resource. By organizing and becoming familiar with underlying patterns, we can improve the security posture of the code we ship. As we reviewed in Chapter 1, we can attribute around 87% of the vulnerabilities found in Node packages to only five vulnerability types. In this chapter, we explore these top five vulnerability types and the coding mistakes that cause them.

1. Insecure Access to Filesystem

Let’s begin by reviewing the most recurring of Node package vulnerabilities that are related to compromising the access to the filesystem or tampering with it. As shown in Figure 2-1, directory traversal and symlink attack are the most prominent types of attacks in this category.

Figure 2-1. Breakdown of insecure filesystem access–related Node vulnerabilities (source: 303 unique insecure filesystem access advisories published by Snyk and npm/Node Security Platform as of April 2018)

Directory Traversal

Directory traversal is a very common security issue found in npm packages. To understand about its disastrous potential, there might not a better example than the Big Bug on npm itself that was caused by an insecure dependency vulnerable to directory traversal. By exploiting it, an attacker could have read any file on the npm server including Secure Sockets Layer (SSL) keys and database passwords with read/write access to the production databases, and potentially replaced packages with malicious payloads.

A common reason behind many occurrences of this vulnerability in npm packages comes down to missing or insufficient user input validation before using it in a URL to serve contents on the server. For example, the following is an insecure implementation of a simple web server that serves files within the public folder:

const http = require('http');
const fs = require('fs');
const path = require('path');
http.createServer(function (req, res) {

 // Get resource path from the user input
 let userInput = req.url;

 // Prevent serving files outside public folder
 let fullPath = (path.join(__dirname, 'public', userInput));

 // Open a file on the server and return its content
 fs.readFile(fullPath, function (err, data) {
  res.writeHead(200, { 'Content-Type': 'text/html' });
  res.write(data);
 return res.end();
 });
}).listen(8080);

While constructing the fullPath to a requested resource, the preceding code example attempts to restrict serving files beyond the web root folder. The code example uses the path.join function to prefix a path for the public folder to a user-supplied path. However, it is not enough to prevent an attacker from accessing files outside of the public folder. An attacker can manipulate the input to contain path traversal characters such as /, ../, or its encoded or double-encoded versions such as %2f, %2e%2e%2f, %2e%2e/, and %252e%252e%252f to navigate and access files outside of the public folder.

Testing directory traversal using curl

The curl command-line tool automatically normalize the request URL by removing any path traversal characters present in it. To circumvent this restriction, use the --path-as-is flag in the curl command to send directory traversal characters. Here is an example:

curl --path-as-is http://localhost:8080/../../../../../etc/passwd

To prevent directory traversal, do not use user input to construct the path of resources on the system. If user input is necessary, accept only a predefined index value for the resource and look up the map to retrieve the actual resource path on the server. If the path needs to be supplied from the user input, as shown in the following example, normalize the user input by using the path.normalize() method before appending it to the resource path URL:

let fullPath = (path.join(__dirname, 'public', path.normalize(userInput)));

The normalize method removes path traversal characters such as . or .. present in input that starts with a / character, such as the value of the req.url property in the preceding example. Note that the normalize method keeps any encoded path traversal characters in the input as-is, thus still posing a risk of path traversal if an application explicitly decodes the user input (using a JavaScript method such as decodeURIComponent) before using it to construct a resource path. For such use cases, validate the input against OS-specific-encoded, double-encoded, UTF-8 encoded, or percent-encoded variations of path traversal characters.

To prevent any potential impact of the directory traversal attack, confine the application user’s access at the system level to only absolutely necessary files and directories.

Symlink Attack

A symlink attack is possible when a package or application uses predictable filenames to write files to shared directories on the host server. Even without root privilege, a malicious user sharing the host server could exploit this vulnerability to corrupt or destroy vital system or application files to which only the victim user has access.

For example, as discovered in a related vulnerability, an insecure version of the cli package writes logs to the shared /tmp directory with a predictable name, such as /tmp/cli.app.log. An attacker could pre-create a symbolic link file /tmp/cli.app.log pointing to /home/user/.bashrc. When the victim application code uses the cli package and the log file /tmp/cli.app.log is generated, the .bashrc file in the application server’s home directory would be overwritten with contents of the log file.

The fix for this symlink attack is simple—don’t write to shared system folders. Another benefit of not writing to shared folders is that it prevents leaking file contents to other users on the host. For example, a related vulnerability was found in the sync-exec package. Affected versions of sync-exec use files located in /tmp/ to buffer command results before returning values. As /tmp/ is almost always set with world-readable permissions, this vulnerability might allow low-privilege users on the system to read the results of commands run via sync-exec under a higher-privilege user.

If you have to use a shared folder for writing nonsensitive data, avoid using predictable filenames. As an example, here is a fix applied for a related vulnerability in the npm package, that uses crypto module’s randomBytes method to generate random filenames.

2. Sensitive Data Exposure

Albeit in a different context, the novelist Henry Green wrote: “The more you leave out, the more you highlight what you leave in.” The same applies to the coding mistakes that expose sensitive information. It could give valuable clues and data to attackers. In the following sections, let’s walk through the pitfalls that often trip up Node developers exposing the sensitive data.

Leaking Application Secrets

Often unintentionally or for convenience during the development phase, application-specific secrets end up at insecure places such as code repositories, log files, client-side storage, URLs, or application global namespace. Anyone with malicious intent can use this information in nefarious ways. Following are the types of assets that are often found leaked in Node packages.

Encryption private keys

These are private keys in text format that a server uses during SSL/Transport Layer Security (TLS) handshake process. Exposing the private key can lead to Man-in-the-Middle (MitM) attacks. Node package vulnerabilities such as the MitM vulnerability in the hotel package) arise from storing these keys in the code repository. Hardware Security Modules (HSMs), encryption and key management tools such as Vault, and cloud-based key management services such as Azure Key Vault or AWS Key Management Service are some ways to securely manage encryption keys.

API tokens

An API token is commonly used to facilitate API authentication via the command line or scripts. Unlike with passwords, we can generate multiple versions of these tokens and revoke them if needed. API tokens are just like passwords, hence they must be protected from appearing in publicly available artifacts such as log files. The token can be compromised if the logs become publicly available. As an example, leaking the GitHub authentication token in the log files was a root cause behind a vulnerabiliy in earlier versions of the grunt-gh-pages package. As a remediation, mask any sensitive data before it appears in the log files. Here are some Node packages that can help with masking. Ensure that you apply masking before the sensitive data is added to the log files.

OAuth bearer tokens

Bearer tokens are API tokens that are used to authenticate access to OAuth-protected resources. Anyone with a bearer token can gain access to the associated resources without demonstrating possession of a cryptographic key. So, to prevent misuse, you must protect bearer tokens from disclosure in storage and in transit. Just as with any other API token, storing bearer tokens in insecure places could compromise access to protected resources. As an example, an affected version of the ghost package was vulnerable to bearer token leakage due to its being stored in the local storage of a browser. If used alongside a Cross-Site Scripting (XSS) attack, a malicious user can hijack the user session. To mitigate the impact of such a leak, use short-lived tokens. You can control the lifetime of a token by setting the validity time field within the protected part of the token. Here are additional details on security considerations to safeguard bearer tokens.

Using Predictable Secrets

Even if not exposed directly, an attacker can guess application-specific secret values if they are weak and predictable. Following are some pitfalls that lead to related vulnerabilities in Node packages.

Insecure randomness

The Math.random() method is often used to generate nonpredictable values such as random tokens, resource IDs, or UUIDs. However, Math.random() is cryptographically insecure. It can produce predictable values and is therefore not safe to use in security-sensitive context. As examples, these are a few Node package vulnerabilities caused by using insecure randomness:

node-uuid

react-native-meteor-oauth

socket.io

uuid

ws

As a safer alternative, as shown in the following code snippet, use the Node crypto module to generate random values because it uses a cryptographically sound source of entropy (that is randomness).

const crypto = require('crypto');
crypto.randomBytes(256, (err, buf) => {
  if (err) throw err;
  // the generated random value can be used here as buf.toString('hex')
  console.log(`${buf.length} bytes of random data: ${buf.toString('hex')}`);
});

In this example, the randomBytes function takes the number of bytes to generate (e.g., 256) and a callback with a Buffer instance to contain the generated bytes.

Nonconstant time comparison

The JavaScript native string comparison operators, including === and ==, typically perform the fail-first string comparison—by iterating the two strings, comparing one character at a time, and stopping when a character differs. Thus, the execution time of a comparison operation is not constant, but instead varies depending on string values. Even though minuscule, this time difference can enable an attacker to guess the secret value being compared one character at a time.

Here are some Node package vulnerabilities that were affected by the timing attack:

node-forge

generator-jhipster

fernet

To prevent this issue, we need to use a constant time string comparison logic that takes the same amount of time regardless of the input values. For instance, the following code example shows a way to do a comparison of two valid string inputs by applying an XOR operator and checking whether the final result is zero.

function constantTimeEquals(strA, strB) {
 if (strA.length !== strB.length) {
  return false;
 } else {
  let equal = 0;
  for (let i = 0; i < strA.length; i++) {
   equal |= strA.charAt(i) ^ strB.charAt(i);
  }
  return equal === 0;
 }
}

Also, these Node packages provide constant time comparison utility.

Memory Exposure

Prior to Node.js 8.0.0, creating a Buffer instance with an integer value as an argument—for example, new Buffer(10)—generates a Buffer with uninitialized underlying memory. The contents of a newly created Buffer remain unknown and might contain sensitive data. This default behavior caused widespread vulnerabilities in Node packages, including:

mongoose

ws

request

sequelize

To address this issue, in Node.js 8.0.0 and above, using a Buffer constructor such as new Buffer(size) returns a zero-filled memory buffer by default. However, using the Buffer constructor is deprecated; instead, as shown in the next example, you should use a safe method Buffer.alloc(size) to create a buffer that is initialized with zeroes:

const buf = Buffer.alloc(5);
console.log(buf);
// Prints: <Buffer 00 00 00 00 00>

Insecure Network Usage

Vulnerabilities arise from Node packages using insecure HTTP protocol to download resources as part of install scripts or at runtime. An attacker can exploit an insecure connection by secretly intercepting and altering the communication—an MitM attack.

This vulnerability is often found in the Node packages that download native binaries written in C/C++ to perform resource-intensive operations. An attacker either on the network or positioned between the user and the remote server can swap out the requested binary with a malicious version while the binary files are being downloaded over HTTP. The attacker-controlled binary can then facilitate remote code execution (RCE) on the victim’s machine.

Package authors should download such resources over HTTPS. To avoid issues with a corporate firewall, it is also helpful to provide an option for users to download dependencies locally and specify the location path as part of an environment variable or a .npmrc configuration parameter. Refer to the node-sass package configuration options for examples.

3. Denial of Service

Denial-of-Service (DoS) attacks are the most disastrous. When successful, an attacker can prevent all legitimate users from accessing the service. Let’s dive straight into reviewing programming sins that could lead to DoS hell.

Unhandled Execution Errors

The single execution thread running an event loop is an Achilles heel for a Node.js system. An unhandled error can result in crashing it, making an otherwise laborious DoS attack feasible with a very little effort. Here are common scenarios that often lead to execution errors in Node packages:

Unhandled errors while processing input values with invalid characters

These errors occur due to unexpected or malicious input values such as Unicode characters, out-of-range values, zero, and negative or possibly large numbers. Here are two examples of related vulnerabilities: connect and mqtt-packet. Using a fuzzing tool such as OWASP ZAP Fuzzer can help in testing the code against such invalid or unexpected data.

Unhandled errors while processing input values with invalid type or shape

An attacker can use mechanisms such as HTTP Parameter Pollution, Query Parameter Coercing, or others to covert a user input to an unexpected type and cause a TypeError on the server, if not handled correctly. Here are examples of related vulnerabilities: ws, yar, hapi. To avoid such errors, use a validator tool such as joi that can check user inputs against a predefined object schema.

Missing or incorrect exception handling from asynchronous code

A Node.js program can communicate an operational error via four different mechanisms: using a throws clause; passing error as a first argument to the callback function declared as (err, value) => ...; rejecting an already returned promise object; or emitting an Error event on an already returned EventEmmiter object. To process the error correctly, we need to be aware of the error delivery mechanism used by the invoked function and handle it accordingly. Here are related DoS vulnerabilities caused by the missing or incorrect handling of error mechanisms: nes, node-http-proxy. In addition, refer to patterns of handling errors in Node.js and Express apps.

Exhausting CPU

Numerous DoS vulnerabilities targeting high CPU usage arise from programs executing a long-running loop whose iterations directly depend on the value of an unvalidated user input. For example, allowing arbitrarily long query strings caused a DoS vulnerability in the node-querystring package.

Another very frequently occurring vulnerability is the Regular Expression Denial of Service (ReDoS). In fact, as shown in Figure 2-2, ReDoS is the most prominent DoS vulnerability in Node packages.

Figure 2-2. Breakdown of DoS vulnerabilities in Node packages (source: 147 unique DoS Node package advisories published as of April 2018).

When exploited, a ReDoS vulnerability could result in CPU overuse while matching a user input against an unsafe regular expression. Such unsafe regular expressions follow a specific pattern, referred to as an evil pattern, that results in increasing execution time of these expressions exponentially when ran against a specially crafted input. For example, as per a vulnerability in an affected version of the brace-expansion package, an unsafe regular expression /^(.*,)+(.+)?$/ caused DoS when matched against a specially crafted input such as ,,,,,,,,,,,,,,,,,,,,,,,,,,,,/n.

Here are some more examples of the ReDoS vulnerabilities in Node packages:

tough-cookie

uri-js

Exhausting Memory

We can find numerous DoS vulnerabilities that cause a program to execute long-running loops and ultimately run out of memory. For example, the following is a code snippet from the earlier version of the ecstatic package that strips null bytes from a user-supplied URL:

while(req.url.indexOf('%00') !== -1) {
  req.url = req.url.replace(/\%00/g, '');
}

The preceding code resulted in a DoS vulnerability in the package. A payload with a large number of null bytes (%00) causes the ecstatic process to crash by running out of memory.

In other cases of memory-specific DoS incidents, the memory exhaustion stems from the code that allocates the unrestricted size of memory directly based on a user input. Here are examples of related vulnerabilities:

node-querystring

faye

fastify

superagent

Another, subtle source of memory issues in Node packages is memory leaks caused by closures. JavaScript closures are inner functions that hold references to members of the enclosing parent function. These references could prevent members in the parent scope from being garbage collected. Here is a great article explaining memory leaks via closures and other sources.

To avoid memory leaks by closures, avoid using anonymous callback functions especially on the hot execution path. Instead, define the callbacks outside of the enclosing parent function. For an example, refer to this commit applied to fix a memory leak vulnerability in the podium package.

Exhausting Filesystem Space

When a user is allowed to upload files without a limit on file sizes, an attacker could exploit this feature by uploading a large number of files and ultimately exhausting the disk space. For example, refer to this vulnerability in affected versions of the ghost package. To avoid it, limit the file size and have a file cleanup strategy.

Exhausting File Descriptors

A file descriptor is an identifier used by the system OS to refer to I/O resources such as an opened file. Most operating systems limit the instances of file descriptors that can be used at any given time.

As shown in the following example, when the fs.open() method is used to open a file, it allocates and returns a new file descriptor in the callback function. After it has been allocated, the file descriptor is used to read data from, write data to, or request information about a file. After the operation is done, it is essential to close the descriptor using the fs.close() method, as shown here:

const fs = require('fs');
fs.open('/open/some/file.txt', 'r', (err, fd) => {
  if (err) throw err;
  fs.fstat(fd, (err, stat) => {
    if (err) throw err;
    // Use stat object that provides information about a file.
    // ...
    // Close the descriptor once operations are completed
    fs.close(fd, (err) => {
      if (err) throw err;
    });
  });
});

If a leak in a program holds references to descriptors or it fails to close the descriptor, eventually the system runs out of descriptors and the application crashes. Here are examples of related vulnerabilities:

kibana

fs

4. Cross Site Scripting

Cross Site Scripting (XSS) is the most prevalent web application security flaw and one of the frequently occurring vulnerabilities in Node packages, as well. An XSS attack involves an attacker inserting JavaScript content on a web page to conduct various malicious actions, most commonly hijacking a user session.

Depending on whether a target web application saves the injected malicious contents on the server, the XSS vulnerability is categorized as reflected (contents rendered without persisting on the server) or stored (contents are saved in a database or in log files and eventually appear in server responses). In another case, referred to as a DOM-based XSS, the malicious contents are injected in a URL fragment and thus do not make a server trip; instead, the vulnerable code running on the user’s browser parses and renders it on the page.

Now let’s review some prominent patterns in known XSS vulnerabilities in Node packages. The majority of these vulnerabilities fall under reflected XSS caused by missing or incorrect escaping (aka output encoding) of user input before incorporating it in the response. For example, following is a code snippet that caused an XSS vulnerability in the restify package. On receiving an invalid request URL, this code sets the invalid URL in the error message without sanitizing it:

callback(new ResourceNotFoundError('%s does not exist', req.url));

As a result, if a browser displays the received error message, it could lead to an XSS vulnerability. An attacker could exploit it in various ways, including adding a script tag as part of the invalid URL.

Most of the XSS instances found in Node packages result from rendering untrusted input in the HTML context of the page; however, we can find a few cases in which unescaped contents ended up in other contexts such as tag attributes, URLs, and JavaScript.

To mitigate the XSS risk in your Node packages, apply both input sanitization and context-based output, escaping where possible. DOMPurify and node-esapi are popular Node packages for the input sanitization and output escaping, respectively. In addition, verify the data type and shape of the user input to avoid an XSS sanitization logic bypass by using HTTP Parameter Pollution or Query Parameter Coercing.

An attacker can creatively disguise the malicious XSS payload in numerous ways (refer to the OWASP XSS Filter Evasion and HTML5 Security cheat sheets for examples) that package authors might miss addressing. Hence, as an application developer, using the Content-Security-Policy (CSP) response header is the easiest and most robust way to mitigate XSS. The CSP header allows developers to specify a whitelist of domains from which the browser is allowed to accept contents. As a result, the browser blocks any inline JavaScript code, external scripts, images, CSS, files, fonts, or media contents injected by an attacker. The helmet package offers middleware to add CSP headers in Express and Koa applications.

CSP Pitfalls

Be aware of CSP pitfalls and bypasses when you configure it.

Use the CSP Evaluator or CSP Validator tool to test the policy before applying it.

5. Injection

An injection vulnerability manifests when the application code sends an untrusted user input to an interpreter without sufficient validations. Attackers can exploit this vulnerability by crafting a hostile input value that tricks the interpreter into executing it as code or a command. This vulnerability can be critical and very damaging because an attacker can potentially exploit it to gain full access to the data, system files, or cause a DoS attack.

Figure 2-3 shows the breakdown of injection vulnerabilities found in Node packages by their types.

Figure 2-3. Breakdown of injection vulnerabilities in Node packages (source: 95 unique injection advisories published by Snyk and npm/Node Security Platform as of April 2018)

Let’s review these types of injection vulnerabilities in more detail.

Arbitrary Code Injection

We can find numerous npm packages vulnerable to arbitrary code injection due to unsafe use of eval family methods—eval(), new Function(), setTimeout(), and setInterval()—to parse user inputs in string format without sufficient input validation.

Here are examples of malicious user inputs that can cause a DoS attack when parsed: while(1), process.exit(), and process.kill(process.pid). An attacker can also exploit this vulnerability by reading files on the server or writing and executing harmful binary files using the fs and child_process modules.

The best fix for this vulnerability is to avoid using the eval family functions where possible. Here are some general guidelines:

Instead of using eval use JSON.parse to convert a JSON format input string into an Object.

Use type-related parseXXX() methods for type conversions.

As fixed in bassmaster, use the bracket notation to access an object’s property with a dynamic name instead of parsing the assignment code as a string.

For cases in which it is not possible to avoid eval family functions, validate user input with regular expressions before using it. You can also apply techniques such as recursive descent or state machine as demonstrated in JSON parsers.

Command Injection

A command injection vulnerability can cause severe damage because it allows an attacker to execute any OS commands on the host server with the same privileges as the vulnerable application and potentially gain full control of the server.

A common mistake that made several Node packages vulnerable to command injection is an unsafe use of the child_process.exec method. child_process.exec accepts the command argument in a string format, which is processed directly by the shell. When an unsanitized user input is used to construct a command, an attacker can exploit it by crafting a malicious payload with a system command in it. For example, the following code snippet shows usage of the child_process.exec method to invoke a gzip command constructed with a dynamic file path taken from user input:

child_process.exec('gzip ' + req.body.file_path,
 function (err, data) {
  console.log(data);
 });

To exploit the injection vulnerability in the preceding code, an attacker can append ; rm -rf /, for instance, to the file_path input. This allows an attacker to break out of the gzip command’s context and execute a malicious command that deletes all of the files on the server. An attacker can even chain multiple commands using shell metacharacters such as ;, &, &&, |, ||, $(), <, >, and >>.

To avoid command injection, use safer alternatives to exec such as execFile or spawn in the child_process module. These methods require that the command and its arguments passed separately in different arguments. As a result, any malicious command arguments passed in a user input are simply ignored or cause a syntax error if they’re not relevant to the target command, thus foiling the command injection attempts. An exception to this behavior is when these methods are used to execute a custom script that processes user inputs or execute certain OS commands (such as find, awk, or sed) that allow passing options to enable file read/write. Therefore, in addition to avoiding exec, you should do the following:

Validate user inputs, preferably against a whitelist

Limit user privileges used by the application to reduce attack surface in an event of successful command injection

SQL Injection

By exploiting a SQL injection vulnerability, an attacker can execute malicious commands on a database to steal sensitive data, tamper with stored data, execute database administration operations, access contents of files present on the database server, and, in some cases, issue commands to the host OS.

A major contributor to SQL injection vulnerability is using unsafe dynamic queries that include user-supplied inputs without sufficient validations. When malicious data is concatenated to a SQL query, the SQL interpreter fails to distinguish between an intended command and the malicious input data, resulting in the execution of the malicious data as SQL commands. Any unsafe user input placed in any SQL clauses such as limit, order by, or like can be a source of the injection attack.

To prevent SQL injection, validate user input, preferably by using a whitelist approach, and use prepared statements instead of creating dynamic queries with string concatenation.

Other Types of Injection Attacks

We can also find Node packages vulnerable to malicious payload injection at unusual contexts such as LDAP, XML, CSV, and HTTP headers. However, the root cause behind each scenario remains the same: missing or insufficient user input validation before sending the input to the corresponding interpreter. Hence, anytime you need to process untrusted data, sanitize or encode it as per the target context before using it.

There are numerous ways by which an injection attack can manifest in Node package dependencies. To reduce its impact, apply security principles such as adding multiple layers of defenses, allowing least privilege, and minimizing attack surface area.

Conclusion

This chapter covered the top five frequently found vulnerability types in Node packages, and coding pitfalls causing them. Knowing these details could help Node developers to avoid similar vulnerabilities in their own code.

To continue on the same path and learn about other Node package vulnerability types, refer to node.advisories.io.

About the Author

Chetan Karande is a full-stack web developer, a security researcher, the author of Securing Node Applications (O’Reilly), and a contributor to multiple open source projects. He is a member of the Open Web Application Security Project (OWASP) and a project leader for the OWASP NodeGoat project, an open source learning platform for Node.js security.