Back to Blog
high SEVERITY8 min read

How Prototype Pollution happens in Node.js async libraries and how to fix it

A high-severity prototype pollution vulnerability (CVE-2021-43138) was discovered in the `async` npm package versions prior to 3.2.2, affecting the `node-red-contrib-opcua` project. By exploiting crafted input passed through async's utility functions, an attacker could corrupt JavaScript's `Object.prototype`, potentially enabling privilege escalation or remote code execution. Upgrading `async` from `3.2.1` to `^3.2.2` in both `package.json` and `package-lock.json` eliminates the attack surface e

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

CVE-2021-43138 is a high-severity Prototype Pollution vulnerability (CWE-1321) in the `async` npm package affecting versions before 3.2.2 and 2.6.4. In Node.js applications, an attacker can supply a specially crafted object with a `__proto__` or `constructor.prototype` key to async's iteration functions, silently modifying `Object.prototype` and affecting all objects in the process. The fix is to upgrade `async` to `^3.2.2` (or `2.6.4` for the v2 branch) in `package.json` and regenerate `package-lock.json` to pin the patched integrity hash.

Vulnerability at a Glance

cweCWE-1321
fixUpgrade async from 3.2.1 to ^3.2.2 in package.json and package-lock.json
riskAttacker-controlled object keys can overwrite Object.prototype, enabling privilege escalation or RCE
languageJavaScript / Node.js
root causeasync's utility functions merged user-supplied keys without filtering __proto__ or constructor properties
vulnerabilityPrototype Pollution

How Prototype Pollution Happens in Node.js async Libraries and How to Fix It


The Incident

In the node-red-contrib-opcua project — a Node-RED plugin for OPC-UA industrial automation — the Trivy scanner flagged a high-severity vulnerability in a seemingly innocuous dependency: the async npm package pinned at version 3.2.1 inside package-lock.json. The vulnerability, CVE-2021-43138, is a classic Prototype Pollution flaw that could allow an attacker to silently corrupt JavaScript's global object prototype, potentially turning a routine async iteration call into a remote code execution vector.

This post breaks down exactly what went wrong, how the attack works in the context of this application, and what the fix does at the code level.


The Vulnerability Explained

What Is Prototype Pollution?

Every JavaScript object inherits properties from Object.prototype. If an attacker can inject a property into Object.prototype — say, isAdmin: true — then every object in the running Node.js process suddenly has isAdmin set to true, unless it explicitly overrides it. This is prototype pollution.

The attack vector is deceptively simple: pass an object with a key like __proto__ or constructor to a function that naively merges or iterates over object keys without filtering them.

The Vulnerable Code Pattern in async 3.2.1

The async library (version 3.2.1, pinned in package-lock.json) contained internal object-manipulation logic in several of its utility functions — including mapValues, reduce, and related iterators — that did not sanitize incoming keys. A simplified representation of the vulnerable pattern looks like this:

// Vulnerable pattern inside async 3.2.1 (simplified)
function mapValues(obj, iteratee, callback) {
  var keys = Object.keys(obj); // does NOT filter __proto__
  var result = {};
  each(keys, function(key, next) {
    iteratee(obj[key], key, function(err, value) {
      result[key] = value; // assigns to result[key] — including __proto__
      next(err);
    });
  }, function(err) {
    callback(err, result);
  });
}

The critical flaw: Object.keys() in older V8 environments and certain edge cases — combined with how async assigned back to result[key] — could be exploited to set result["__proto__"]["polluted"] = true, which propagates to Object.prototype.

The vulnerable version was locked in package-lock.json with this exact entry:

"node_modules/async": {
  "version": "3.2.1",
  "resolved": "https://registry.npmjs.org/async/-/async-3.2.1.tgz",
  "integrity": "sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==",
  "license": "MIT"
}

How This Could Be Exploited in node-red-contrib-opcua

node-red-contrib-opcua is an industrial IoT plugin. It processes OPC-UA messages — structured data objects that can arrive from external devices or over a network. If any part of the message-handling pipeline passes an attacker-controlled object (e.g., a crafted OPC-UA node configuration or a malicious Node-RED flow payload) through one of async's iteration functions, the attacker could inject:

// Attacker-crafted input object
{
  "__proto__": {
    "admin": true,
    "isAuthenticated": true
  }
}

Once Object.prototype is polluted, any subsequent code that checks obj.admin or obj.isAuthenticated without explicit prototype checks would return true — even for freshly created objects. In a Node-RED environment where flows dynamically evaluate node properties, this could bypass authorization logic or alter control flow in dangerous ways.

Real-world impact for this application:
- Bypass of permission checks in Node-RED's flow execution engine
- Corruption of OPC-UA session state objects
- Potential for denial-of-service if polluted properties break internal object assumptions
- In worst-case scenarios involving eval or dynamic property access downstream: remote code execution


The Fix

What Changed

The fix is a targeted dependency upgrade across two files: package.json and package-lock.json.

package.json — Before:

"async": "3.2.1",

package.json — After:

"async": "^3.2.2",

The change from an exact pin (3.2.1) to a caret range (^3.2.2) is deliberate: it allows npm to automatically pick up future patch releases within the 3.x minor line, reducing the window of exposure for similar future vulnerabilities.

package-lock.json — Before:

"node_modules/async": {
  "version": "3.2.1",
  "resolved": "https://registry.npmjs.org/async/-/async-3.2.1.tgz",
  "integrity": "sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==",
  "license": "MIT"
}

package-lock.json — After:

"node_modules/async": {
  "version": "3.2.2",
  "resolved": "https://registry.npmjs.org/async/-/async-3.2.2.tgz",
  "integrity": "sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g==",
  "license": "MIT"
}

The integrity hash change is critical: it cryptographically pins the patched tarball. Any attempt to install the old vulnerable version would fail the integrity check, providing a hard guarantee that the vulnerable code is gone.

Why Two Files?

  • package.json defines the intent — what version range the project wants.
  • package-lock.json defines the reality — the exact resolved version and its verified hash.

Updating only package.json would leave the lockfile pointing to 3.2.1 in environments that respect lockfiles strictly (like most CI/CD pipelines using npm ci). Both files must be updated to guarantee the patched version is actually installed everywhere.

What async 3.2.2 Changed Internally

The patch in async 3.2.2 added explicit key filtering to prevent __proto__, constructor, and prototype from being treated as regular object keys during merge and iteration operations. The fix follows the same pattern recommended by the Node.js security team:

// Patched approach (conceptual — async 3.2.2)
function isSafeKey(key) {
  return key !== '__proto__' && key !== 'constructor' && key !== 'prototype';
}

// Keys are now filtered before assignment
keys.filter(isSafeKey).forEach(function(key) {
  result[key] = transformedValue;
});

This ensures that even if an attacker passes a crafted object with __proto__ as a key, it is silently dropped rather than merged into the result object.


Key Takeaways

  • Pinning async to 3.2.1 in package-lock.json was the direct cause: the exact integrity hash locked in the vulnerable tarball, preventing automatic security updates from taking effect.
  • The __proto__ key is the classic prototype pollution vector: any library that iterates or merges object keys without filtering it is potentially vulnerable — always check changelogs when upgrading utility libraries.
  • Both package.json and package-lock.json must be updated: changing only one file leaves the vulnerability intact in strict-lockfile environments like npm ci.
  • Industrial IoT contexts amplify the risk: node-red-contrib-opcua processes external device data, meaning attacker-controlled objects can realistically reach async's iteration functions through crafted OPC-UA payloads.
  • Caret ranges (^3.2.2) over exact pins for security-sensitive packages: the updated package.json now allows automatic uptake of future 3.x security patches without requiring a manual PR for every fix.

How Orbis AppSec Detected This

  • Source: External OPC-UA message payloads and Node-RED flow configurations processed by node-red-contrib-opcua, which can contain attacker-controlled object keys.
  • Sink: async 3.2.1's internal object iteration and merge functions (e.g., mapValues, reduce) in node_modules/async, called with user-influenced data objects — flagged via the node_modules/async entry in package-lock.json.
  • Missing control: The async 3.2.1 library did not filter __proto__, constructor, or prototype keys before performing object property assignments, and the project had no sanitization layer before passing external data to async utility functions.
  • CWE: CWE-1321 — Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution').
  • Fix: Upgraded async from the exact pin 3.2.1 to ^3.2.2 in package.json and regenerated package-lock.json with the patched version's cryptographic integrity hash.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

CVE-2021-43138 is a reminder that prototype pollution doesn't require complex exploit chains — a single unfiltered object key passed to a widely-used utility library is enough. In the node-red-contrib-opcua project, the vulnerable async 3.2.1 package sat quietly in package-lock.json, locked to a specific tarball hash that guaranteed the vulnerable code was always installed.

The fix is surgical and low-risk: upgrading to async ^3.2.2 closes the vulnerability while the caret range ensures future security patches are picked up automatically. For developers building Node.js applications — especially those processing external or industrial data — regularly auditing your dependency tree and integrating SCA tools into CI is not optional. The attack surface of your application includes every line of code in node_modules.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #900

Related Articles

critical

LDAP Filter Injection in da_unique_email_validator Fixed

The registration-time email uniqueness validator, `da_unique_email_validator`, formatted the submitted email address straight into an LDAP search filter with Python's `%` operator, so filter metacharacters in the email were interpreted as filter syntax. The fix wraps the value in `ldap.filter.escape_filter_chars()` (and imports the `ldap.filter` submodule explicitly), so a submitted address is always treated as a literal attribute value. Any deployment with `ldap login` enabled and a bind accoun

high

installPlugin(): Unvalidated npm Package Names Reach npm install

A plugin manager service exposed an `installPlugin(plugin: PluginInfo)` method that passed `plugin.packageName` and `plugin.version` straight into the platform's npm install routine with no validation, no blocklist, and no integrity verification of the fetched tarball. Because npm treats a non-semver "version" as a fetch specifier — a tarball URL, a git ref, a local path — an attacker who could influence the plugin listing could get arbitrary code installed and executed with full Electron/Node p

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

critical

eval() in Async Function Constructor Enables Runtime Escape

The eval.mjs command handler used raw `eval()` to execute JavaScript expressions, creating a critical code injection path if owner credentials are compromised. The fix replaces `eval()` with the `AsyncFunction` constructor and explicitly shadows `process`, `require`, and other runtime globals as parameters, preventing evaluated code from reaching the Node.js runtime even when authentication boundaries fail.

high

How Regular Expression Denial of Service (ReDoS) Happens in Node.js trim-newlines and How to Fix It

CVE-2021-33623 exposed a Regular Expression Denial of Service (ReDoS) vulnerability in the npm package `trim-newlines` versions 1.0.0 and earlier. The vulnerable `.end()` method used an inefficient regex pattern that could cause severe performance degradation when processing malicious input. Upgrading to version 4.0.1 patches the regex implementation and eliminates the attack surface.

critical

How CSS Injection via Weak Pattern Validation happens in Vue.js and how to fix it

A critical CSS injection vulnerability in `testpage/App.vue` allowed attackers to bypass weak HTML5 pattern validation and load malicious stylesheets. The fix replaces direct variable assignment with a hardened `setCustomStylesheetHref()` method using strict regex validation.