Node.js — Security Best Practices (original) (raw)

Intent

This document intends to extend the current threat model and provide extensive guidelines on how to secure a Node.js application.

Document Content

Threat List

The Node.js threat model defines what is or is not considered a_vulnerability in Node.js itself_. Some of the topics below are not vulnerabilities in Node.js core according to that model, but they are still important application-level threats that you should account for when building and operating Node.js software.

Denial of Service of HTTP server (CWE-400)

This is an attack where the application becomes unavailable for the purpose it was designed due to the way it processes incoming HTTP requests. These requests need not be deliberately crafted by a malicious actor: a misconfigured or buggy client can also send a pattern of requests to the server that result in a denial of service.

HTTP requests are received by the Node.js HTTP server and handed over to the application code via the registered request handler. The server does not parse the content of the request body. Therefore any DoS caused by the contents of the body after they are handed over to the request handler is not a vulnerability in Node.js itself, since it's the responsibility of the application code to handle it correctly.

Ensure that the WebServer handles socket errors properly, for instance, when a server is created without an error handler, it will be vulnerable to DoS

const  = ('node:net');

const  = .(function () {
  // socket.on('error', console.error) // this prevents the server to crash
  .('Echo server\r\n');
  .();
});

.(5000, '0.0.0.0');

If a bad request is performed the server could crash.

An example of a DoS attack that is not caused by the request's contents isSlowloris. In this attack, HTTP requests are sent slowly and fragmented, one fragment at a time. Until the full request is delivered, the server will keep resources dedicated to the ongoing request. If enough of these requests are sent at the same time, the amount of concurrent connections will soon reach its maximum resulting in a denial of service. This is how the attack depends not on the request's contents but on the timing and pattern of the requests being sent to the server.

Mitigations

DNS Rebinding (CWE-346)

This is an attack that can target Node.js applications being run with the debugging inspector enabled using the --inspect switch.

Since websites opened in a web browser can make WebSocket and HTTP requests, they can target the debugging inspector running locally. This is usually prevented by the same-origin policy implemented by modern browsers, which forbids scripts from reaching resources from different origins (meaning a malicious website cannot read data requested from a local IP address).

However, through DNS rebinding, an attacker can temporarily control the origin for their requests so that they seem to originate from a local IP address. This is done by controlling both a website and the DNS server used to resolve its IP address. See DNS Rebinding wiki for more details.

Mitigations

All the files and folders included in the current directory are pushed to the npm registry during the package publication.

There are some mechanisms to control this behavior by defining a blocklist with.npmignore and .gitignore or by defining an allowlist in the package.json

Mitigations

HTTP Request Smuggling (CWE-444)

This is an attack that involves two HTTP servers (usually a proxy and a Node.js application). A client sends an HTTP request that goes first through the front-end server (the proxy) and then is redirected to the back-end server (the application). When the front-end and back-end interpret ambiguous HTTP requests differently, there is potential for an attacker to send a malicious message that won't be seen by the front-end but will be seen by the back-end, effectively "smuggling" it past the proxy server.

See the CWE-444 for a more detailed description and examples.

Since this attack depends on Node.js interpreting HTTP requests differently from an (arbitrary) HTTP server, a successful attack can be due to a vulnerability in Node.js, the front-end server, or both. If the way the request is interpreted by Node.js is consistent with the HTTP specification (see RFC7230), then it is not considered a vulnerability in Node.js.

Mitigations

Information Exposure through Timing Attacks (CWE-208)

This is an attack that allows the attacker to learn potentially sensitive information by, for example, measuring how long it takes for the application to respond to a request. This attack is not specific to Node.js and can target almost all runtimes.

The attack is possible whenever the application uses a secret in a timing-sensitive operation (e.g., branch). Consider handling authentication in a typical application. Here, a basic authentication method includes email and password as credentials. User information is retrieved from the input the user has supplied from ideally a DBMS. Upon retrieving user information, the password is compared with the user information retrieved from the database. Using the built-in string comparison takes a longer time for the same-length values. This comparison, when run for an acceptable amount unwillingly increases the response time of the request. By comparing the request response times, an attacker can guess the length and the value of the password in a large quantity of requests.

Mitigations

Malicious Third-Party Modules (CWE-1357)

According to the Node.js threat model, scenarios that require a malicious third-party module are not considered vulnerabilities in Node.js core, because Node.js treats the code it is asked to run (including dependencies) as trusted. However, malicious or compromised dependencies remain one of the most critical application-level risks for Node.js users and should be treated as such.

Currently, in Node.js, any package can access powerful resources such as network access. Furthermore, because they also have access to the file system, they can send any data anywhere.

All code running into a node process has the ability to load and run additional arbitrary code by using eval()(or its equivalents). All code with file system write access may achieve the same thing by writing to new or existing files that are loaded.

Examples

Be sure to pin dependency versions and run automatic checks for vulnerabilities using common workflows or npm scripts. Before installing a package make sure that this package is maintained and includes all the content you expected. Be careful, the GitHub source code is not always the same as the published one, validate it in the node_modules.

Supply chain attacks

A supply chain attack on a Node.js application happens when one of its dependencies (either direct or transitive) are compromised. This can happen either due to the application being too lax on the specification of the dependencies (allowing for unwanted updates) and/or common typos in the specification (vulnerable to typosquatting).

An attacker who takes control of an upstream package can publish a new version with malicious code in it. If a Node.js application depends on that package without being strict on which version is safe to use, the package can be automatically updated to the latest malicious version, compromising the application.

Dependencies specified in the package.json file can have an exact version number or a range. However, when pinning a dependency to an exact version, its transitive dependencies are not themselves pinned. This still leaves the application vulnerable to unwanted/unexpected updates.

Possible attack vectors:

Mitigations

Memory Access Violation (CWE-284)

Memory-based or heap-based attacks depend on a combination of memory management errors and an exploitable memory allocator. Like all runtimes, Node.js is vulnerable to these attacks if your projects run on a shared machine. Using a secure heap is useful for preventing sensitive information from leaking due to pointer overruns and underruns.

Unfortunately, a secure heap is not available on Windows. More information can be found on Node.js secure-heap documentation.

Mitigations

Monkey Patching (CWE-349)

Monkey patching refers to the modification of properties in runtime aiming to change the existing behavior. Example:

.. = function () {
  // overriding the global [].push
};

Mitigations

The --frozen-intrinsics flag enables experimental¹frozen intrinsics, which means all the built-in JavaScript objects and functions are recursively frozen. Therefore, the following snippet will not override the default behavior ofArray.prototype.push

.. = function () {
  // overriding the global [].push
};

// Uncaught:
// TypeError <Object <Object <[Object: null prototype] {}>>>:
// Cannot assign to read only property 'push' of object ''

However, it’s important to mention you can still define new globals and replace existing globals using globalThis

> globalThis.foo = 3; foo; // you can still define new globals
3
> globalThis.Array = 4; Array; // However, you can also replace existing globals
4

Therefore, Object.freeze(globalThis) can be used to guarantee no globals will be replaced.

Prototype Pollution Attacks (CWE-1321)

Per the Node.js threat model, prototype pollution that relies on an attacker controlling user input is not considered a vulnerability in Node.js core, because Node.js trusts the inputs provided by application code. Nonetheless, prototype pollution is a serious class of vulnerabilities for Node.js applications and third-party libraries, and you should implement defenses at the application and dependency level.

Prototype pollution refers to the possibility of modifying or injecting properties into Javascript language items by abusing the usage of __proto_ , _constructor, prototype, and other properties inherited from built-in prototypes.

const  = { : 1, : 2 };
const  = .('{"__proto__": { "polluted": true}}');

const  = .({}, , );
.(.polluted); // true

// Potential DoS
const  = .('{"__proto__": null}');
const  = .(, );
.hasOwnProperty('b'); // Uncaught TypeError: d.hasOwnProperty is not a function

This is a potential vulnerability inherited from the JavaScript language.

Examples:

Additional scenarios include:

Mitigations

Uncontrolled Search Path Element (CWE-427)

The Node.js threat model considers the file system in the environment accessible to Node.js as trusted. As a result, issues that rely solely on controlling files in those locations are not considered vulnerabilities in Node.js core. They are, however, relevant to the security of your overall deployment and supply chain, so you should harden your environment and use the mechanisms below to reduce risk.

Node.js loads modules following the Module Resolution Algorithm. Therefore, it assumes the directory in which a module is requested (require) is trusted.

By that, it means the following application behavior is expected. Assuming the following directory structure:

If server.js uses require('./auth') it will follow the module resolution algorithm and load auth instead of auth.js.

Node.js Permission Model

Node.js provides a permission modelthat can be used to restrict what a given process is allowed to do at runtime. This model complements the Node.js threat model.

When enabled (for example, using the --permission flag), the permission model lets you selectively allow or deny access to sensitive capabilities such as:

This can help contain the impact of malicious or compromised dependencies, untrusted configuration, or unexpected behavior in your own code, since even trusted code will be prevented from performing actions outside the permissions you have explicitly granted.

Refer to the Node.js permissions documentation for up-to-date flags and options.

Experimental Features in Production

The use of experimental features in production isn't recommended. Experimental features can suffer breaking changes if needed, and their functionality isn't securely stable. Although, feedback is highly appreciated.

The OpenSSF is leading several initiatives that can be very useful, especially if you plan to publish an npm package. These initiatives include:

You can also collaborate with other projects and security experts through the OpenJS Security Collaboration Space.