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
•Technically reviewed by Anupam Mediratta•Published August 26, 2026•Reviewed 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

high

CVE-2026-4800: lodash Template Imports Allow Code Execution

CVE-2026-4800 affects lodash's `_.template()` templating API, where untrusted input reaching the `imports` option can lead to arbitrary code execution. The fix was shipped as a dependency upgrade from lodash 4.17.21 to 4.18.1 in the project's lockfile, though the PR itself notes it was never verified against the actual code paths in this repository.

critical

proxy-addr 2.0.7 IP Spoofing: CVE-2026-90711 Trust Bypass

A critical vulnerability in proxy-addr 2.0.7 allowed attackers to spoof client IP addresses by manipulating X-Forwarded-For headers when the trust chain evaluation contained specific misconfigurations. The fix in version 2.0.8 hardens the trust evaluation logic to prevent IP address falsification in Express.js applications relying on this common middleware dependency.

high

TweenMax `_applyCycle` Prototype Pollution via vars.cycle Keys

A bundled copy of the TweenMax animation library copied attacker-influenceable `vars.cycle` property names straight onto a tween configuration object using an unguarded `for...in` loop, so a key named `__proto__`, `constructor`, or `prototype` was written through to the object's prototype chain. The fix adds an explicit key denylist to both copies of the `_applyCycle` helper so those three names are skipped during the merge. No CVE or GHSA is assigned; the issue is tracked as CWE-1321 (Improperl

high

picomatch 2.3.1 ReDoS: Extglob Pattern Catastrophic Backtracking

picomatch versions below 2.3.2, 3.0.2, or 4.0.4 contain a Regular Expression Denial of Service vulnerability in extglob pattern parsing. An attacker can cause catastrophic backtracking with patterns containing nested alternations and quantifiers, freezing any Node.js process that evaluates untrusted glob expressions.

critical

pet-window.js Dynamic Code Evaluation: CWE-94 Hardening via Number

The pet-window module constructed dynamic JavaScript by embedding raw configuration values into code strings. An attacker with local access could inject arbitrary JavaScript by modifying stored configuration. The fix replaces string interpolation with explicit Number() coercion and NaN validation for all numeric configuration parameters.

high

sanitizeUnicodeInput(): Fullwidth U+ Bypasses Codepoint Validation

The `sanitizeUnicodeInput()` helper used by the project character-range settings screen rewrote `U+` prefixes to `0x` and called `parseInt()`, but never normalized its argument first. Compatibility-equivalent forms such as fullwidth `U+`, superscript digits, or mathematical alphanumerics never matched the `/U\+/gi` regex, fell through to the `else return inputString` branch, and were handed back to callers verbatim as "sanitized" values. The fix inserts a `String.prototype.normalize('NFKC')` pas