The Node.js runtime environment not only accelerates JavaScript applications and supports scalability, but it also introduces various vulnerabilities. These range from broken access control, cryptography failures, and injection attacks to server-side request forgery. You must know how to mitigate these risks in order to secure applications that use Node.js.
In this blog, we'll identify the most common Node.js vulnerabilities and explain how to mitigate them. After reviewing what Node.js is, we'll cover the following vulnerabilities:
- Denial of service (DoS) of HTTP server
- DNS rebinding
- Exposure of sensitive information to an unauthorized actor
- HTTP request smuggling
- Information exposure through timing attacks
- Malicious third-party modules
- Supply chain attacks
- Memory access violation
- Monkey patching
- Prototype pollution attacks
- Uncontrolled search path element
- Experimental features in production
What is Node.js?
Node.js is an asynchronous event-driven JavaScript runtime environment that allows developers to create servers, apps, command-line tools, and server-side code for multiple platforms. It enables running JavaScript on your server independently of a browser. This asynchronous functionality allows the main program to continue running while awaiting client requests, improving throughput and scalability.
Developers often use Node.js to create dynamic web pages that adapt content to users. Node.js also enables developers to simplify coding by using JavaScript for both server-side and client-side scripting, a paradigm known as "JavaScript everywhere".
Key facts about Node.js:
- Written primarily in C++ and built on Google’s V8 JavaScript engine.
- Cross-platform compatibility: Linux, macOS, Windows, IBM AIX, IBM i, and FreeBSD.
- Governed by the OpenJS Foundation under the Linux Foundation’s Collaborative Projects program.
Most Common Node.js Vulnerabilities and Mitigations
Node.js security best practices identify a dozen common vulnerabilities, classified by their MITRE Common Weakness Enumeration (CWE) categorization number. Additionally, the Open Worldwide Application Security Project (OWASP) has reviewed the 10 top Node.js vulnerabilities. Here are some of the most common Node.js vulnerabilities you're likely to encounter and how to mitigate them. We've included their CWE numbers where applicable so you can consult MITRE documentation for additional information.
1. Denial of Service of HTTP Server (CWE-400)
This vulnerability, also known as uncontrolled resource consumption, occurs when inefficient processing of incoming HTTP requests overloads server resources leading the applications to slow down, crash, or lock out users. Excessive resource requests can originate with client misconfigurations, with client errors, or with bad actors sending malicious requests. Servers can be vulnerable to DoS attacks when they lack error handling mechanisms or confusion obscures which part of the program handles releasing resources.
To mitigate this risk:
- Use a reverse proxy server to receive client requests and send them to Node.js applications.
- Configure server timeouts so idle connections or slow incoming requests can be dropped.
- Limit open sockets per host and total open sockets.
Echo Server Example with Error Handling
const net = require('node:net');
const server = net.createServer(function (socket) {
// socket.on('error', console.error) // this keeps the server from crashing
socket.write('Echo server\r\n');
socket.pipe(socket);
});
server.listen(5000, '0.0.0.0');
2. DNS Rebinding (CWE-346)
This attack, a variant of origin validation error, targets a vulnerability opened when the Node.js Inspector debugging interface is enabled. The Inspector has full access to the Node.js execution environment, so bad actors who access it can execute malicious code. Node.js starts listening for debugging messages upon receiving a SIGUSR1 user-defined signal.
Most browsers mitigate this by forbidding scripts from reaching resources from multiple origins, so malicious sites can't access data requested from a local IP address. However, DNS rebinding bypasses this safeguard by hijacking both websites and DNS servers that resolve their IP addresses, effectively controlling request origins so they seem to come from local IP addresses.
To mitigate this vulnerability:
- Disable SIGUSR1 signal from activating Inspector by attaching a process.on(‘SIGUSR1’, …) listener.
- Don't run Inspector in production.
Example of Signal Handling
process.on('SIGUSR1', function() { something; })
3. Exposure of Sensitive Information to an Unauthorized Actor (CWE-552)
JavaScript's package manager npm serves as the default package manager for Node.js and comes recommended with the Node.js installer. During package publication, all files and folders in the current directory may be pushed to the npm registry, exposing sensitive files to unauthorized users. This vulnerability frequently emerges during caching.
Adopt the following npm security best practices to mitigate this vulnerability:
- Use the package.json files property to create an allowlist of files to be published and/or use npm's npmignore and/or GitHub's gitignore tools to create a blocklist of files to exclude from publication. Note that the files property takes precedence over ignore files in package roots but not subdirectories, and if you use both npmignore and gitignore files without an allowlist, anything not listed in npmignore gets published. If you use both ignore files, make sure to keep both of them updated.
- Test before publishing by running the npm-publish dry-run command, which reports what would have happened upon publication without making any changes.
- Unpublish any packages with sensitive exposed files.
Example
{
"name": "cli-ux",
"main": "./lib/index.js",
"files": [
"/lib"
]
}
npm publish --dry-run
4. HTTP Request Smuggling (CWE-444)
This vulnerability can occur when web clients or servers interpret messages differently than proxy servers or other intermediaries. When this happens, a bad actor may be able to conceal a malicious message from a proxy server in order to attack a Node.js application. The root of the vulnerability can lie with the proxy server, the Node.js application, or both.
To mitigate this vulnerability:
- Use HTTP/2 end-to-end and disable HTTP downgrading.
- When using Node.js to create HTTP servers, avoid using the --insecure-http-parser command line option, which accepts invalid HTTP headers.
- Configure proxy servers to normalize ambiguous requests.
- Monitor both Node.js and proxy servers for HTTP request smuggling vulnerabilities.
Example
// file: ./index.js
const http2 = require('http2')
// The `http2.connect` method creates a new session with example.com
const session = http2.connect('https://example.com')
// If there is any error in connecting, log it to the console
session.on('error', (err) => console.error(err))
5. Information Exposure through Timing Attacks (CWE-208)
Like all runtime environments, Node.js can be vulnerable to timing attacks, a variety of side-channel attacks which exploits observable time differences between operations to infer sensitive information. For example, by comparing normal response times with authentication response times, attackers can deduce password length and test character values, eventually discovering the correct password.
To mitigate this vulnerability:
- Avoid using secrets in operations that consume a variable amount of time.
- Use the crypto.timingSafeEqual(a, b) function to apply constant-time comparison, which sets a constant execution time independent of value, preventing leaking of sensitive information.
- Use the crypto.scrypt(password, salt, keylen[, options], callback) function to activate anonymous scrypt implementation, designed to frustrate brute-force attacks by tying up attacker resources.
Example
import crypto from 'crypto';
const a = Buffer.alloc(5, 'b');
const b = Buffer.alloc(5, 'b');
let res = crypto.timingSafeEqual(a, b);
console.log(res);
const {
scrypt,
} = require('node:crypto');
// Using the factory defaults.
scrypt('password', 'salt', 64, (err, derivedKey) => {
if (err) throw err;
console.log(derivedKey.toString('hex')); // '3745e48...08d59ae'
});
// Using a custom N parameter. Must be a power of two.
scrypt('password', 'salt', 64, { N: 1024 }, (err, derivedKey) => {
if (err) throw err;
console.log(derivedKey.toString('hex')); // '3745e48...aa39b34'
});
6. Malicious Third-Party Modules (CWE-1357)
Node.js allows any package to access resources or send data. This allows the JavaScript eval() method, similar methods, or any code with file system write access to load and run malicious code.
To mitigate this vulnerability:
- Enable the --experimental-permission model to restrict access to file systems through the fs module, spawn processes, use node:worker_threads, use native addons, use WASI, and enable the Inspector.
- Follow secure coding practices when using third-party modules.
- Only use current modules with recent updates and security patches.
- Verify that any GitHub source code matches published code.
- Pin dependencies to specific versions.
- Run automated vulnerability checks with tools such as npm-audit.
Example
$ node --experimental-permission index.js
node:internal/modules/cjs/loader:171
const result = internalModuleStat(receiver, filename);
^
Error: Access to this API has been restricted
at stat (node:internal/modules/cjs/loader:171:18)
at Module._findPath (node:internal/modules/cjs/loader:627:16)
at resolveMainPath (node:internal/modules/run_main:19:25)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:24)
at node:internal/main/run_main_module:23:47 {
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemRead',
resource: '/home/user/index.js'
}
7. Supply chain attacks (CWE informal grouping, no formal number)
Various Node.js supply chain vulnerabilities can emerge from compromised dependencies. Typically, they occur when a Node.js application has failed to specify dependencies precisely, allowing insecure updates, or when common typos leave the specification open to typesquatting. The Node.js ecosystem is vulnerable to a specific type of attack called lockfile poisoning which exploits the use of package-lock.json configuration to pin all dependencies to a specific version, location, and integrity hash. A lockfile that has been compromised through lax review or malicious injection can spread vulnerabilities throughout your Node.js ecosystem.
To mitigate this vulnerability:
- Prohibit arbitrary script execution by using the --ignore-scripts commandline utility, which can be set globally by using the npm-config command to set ignore-scripts to true.
- Pin dependencies to specific immutable versions rather than ranges or mutable versions.
- If you use lockfiles to pin dependencies, mitigate lockfile poisoning by reviewing lockfiles, using a tool such as lockfile-lint to check code, and including validation tools in your continuous integration process.
- Automate vulnerability checks with tools such as npm-audit.
- Enforce lockfiles and flag inconsistencies by using npm-ci instead of npm-install.
- Review package.json files for typos in dependency names.
Example
npm config set ignore-scripts true
npx lockfile-lint --path yarn.lock --type yarn --validate-https --allowed-hosts yarnpkg.org
8. Memory Access Violation (CWE-284)
All runtime environments running on shared machines can be targeted by this variant of improper access control. This attack triggers a buffer overflow condition by causing an application to attempt to access memory which doesn't exist or otherwise isn't accessible.
To mitigate this vulnerability:
- Avoid running production applications on shared machines.
- Use the Node.js --secure-heap=n command to control heap size limits. Note that this is not available on Windows.
Example
--secure-heap=n
9. Monkey Patching (CWE-349)
This vulnerability can occur when an attacker alters code behavior at runtime without altering the source code. For example, an attacker may change object or class properties or methods.
To mitigate this vulnerability:
- Use the Object.freeze() method to make existing properties non-writable and non-configurable and disallow extensions.
Example
const obj = {
prop: 42,
};
Object.freeze(obj);
obj.prop = 33;
// Throws an error in strict mode
console.log(obj.prop);
// Expected output: 42
10. Prototype Pollution Attacks (CWE-1321)
This attack introduces or changes JavaScript properties inherited from built-in prototypes. For example, attackers may manipulate the __proto_, _constructor, or prototype properties.
To mitigate prototype pollution vulnerabilities:
- Avoid unsafe recursive merges.
- Use JSON Schema validation to screen external or untrusted requests.
- Use Object.create(null) to create objects without prototypes.
- Use Object.freeze(MyObject.prototype) to freeze prototypes.
- Use the using --disable-proto flag to disable the Object.prototype.__proto__ property.
- Run Object.hasOwn(obj, keyFromObj) to verify that properties exist directly on objects and are not inherited from prototypes.
- Avoid using Object.prototype methods.
Example
let obj = Object.create(null);
11. Uncontrolled Search Path Element (CWE-427)
When Node.js loads modules, it assumes the directory containing the module is trusted. This can allow bad actors to introduce malicious code through locations under the search path. For example, an attacker may be able to modify a /tmp directory or a current working directory. In some cases where the framework identifies dependencies on third-party libraries or other packages and consults a repository containing the required package, it may search a public repository before a private one. An attacker could manipulate this by placing a malicious package in the public repository named after the private repository one.
To mitigate this vulnerability:
- Run automated Static Application Security Testing (SAST) to check source code for this vulnerability by building a model of data and control flow to identify risky patterns connecting input sources with destinations where data interacts with external components.
- Hard-code search paths to sets of known values or restrict permissions for configuration.
- Specify invoked programs through fully-qualified pathnames. To improve portability to systems with different pathnames, refer code to a central location.
- Before invoking other programs, remove or restrict any environment settings, such as the PATH environment variable or LD_LIBRARY_PATH.
- Review search paths for unsafe components, such as current working directories or temporary files directories.
- Use functions that require explicit paths.
- Use the experimental --policy-integrity flag to prevent policy changes. However, note that this flag has been deprecated in recent Node.js updates and can only be used with versions that support it. Also, this flag should not be used in production, for reasons discussed below.
Example
node --experimental-policy-integrity=policy.json app.js
12. Experimental Features in Production (no CWE number)
Speaking of the experimental --policy-integrity flag, the Node.js community has debated dropping experimental features because of unclear boundaries and security expectations. The Node.js security team and the Node.js Next 10 group are currently re-evaluating experimental features in light of the discontinuation of support for features that have remained inactive for an extended time. Because experimental features are unstable and subject to change, you should avoid using them in a production environment.
Secure Your Node.js Application with Cobalt
The numerous Node.js vulnerabilities make proactive security a high priority when developing applications using this runtime environment.
Penetration testing services can help you ensure your application's resilience by quickly finding and fixing flaws. Talk to our team today about how we can help you secure your application's Node.js runtime environment.