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.


Prevention & Best Practices

1. Audit Your Dependency Tree Regularly

Prototype pollution vulnerabilities are almost always introduced through transitive dependencies. Run:

npm audit
# or with more detail:
npx better-npm-audit audit

Add this to your CI pipeline so new vulnerabilities are caught before they reach production.

2. Use npm ci in Production

npm ci installs exactly what is in package-lock.json and fails if there's a mismatch. This prevents silent upgrades to vulnerable versions and ensures the integrity hashes are verified.

# In your CI/CD pipeline:
npm ci --production

3. Freeze Dependency Ranges Carefully

Exact pins ("async": "3.2.1") feel safe but prevent automatic security patches. Caret ranges ("async": "^3.2.2") allow patch and minor updates — a better balance for security-sensitive dependencies. For critical libraries, consider using npm audit fix in CI to auto-apply security patches.

4. Sanitize Object Keys When Merging User Input

If your own code merges external objects, always filter dangerous keys:

const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);

function safeMerge(target, source) {
  for (const key of Object.keys(source)) {
    if (!UNSAFE_KEYS.has(key)) {
      target[key] = source[key];
    }
  }
  return target;
}

Alternatively, use Object.create(null) to create prototype-free objects when handling untrusted data:

const safeObj = Object.create(null);
// This object has NO prototype — pollution is impossible

5. Use a Software Composition Analysis (SCA) Tool

Tools like Trivy (which caught this vulnerability), Snyk, or GitHub Dependabot continuously monitor your dependency tree against known CVE databases. Integrate them into your pull request workflow.

Security Standards

  • OWASP A06:2021 – Vulnerable and Outdated Components: This vulnerability is a textbook example of why keeping dependencies updated is a security requirement, not just a maintenance chore.
  • CWE-1321: Improperly Controlled Modification of Object Prototype Attributes — the formal classification for prototype pollution.

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.


References

Frequently Asked Questions

What is Prototype Pollution?

Prototype Pollution is a JavaScript vulnerability where an attacker injects properties into Object.prototype via specially crafted keys like __proto__ or constructor.prototype, causing those properties to appear on every object in the application.

How do you prevent Prototype Pollution in Node.js?

Use libraries that sanitize or block __proto__ and constructor keys before merging objects, keep dependencies up to date, and validate all external input before passing it to object-merging or iteration functions.

What CWE is Prototype Pollution?

Prototype Pollution maps to CWE-1321: Improperly Controlled Modification of Object Prototype Attributes.

Is input validation alone enough to prevent Prototype Pollution?

Input validation helps but is not sufficient on its own; the underlying library must also sanitize keys internally. Patching the vulnerable dependency (as done here) is the most reliable fix.

Can static analysis detect Prototype Pollution?

Yes. Tools like Semgrep, Snyk, and Trivy (which flagged this issue) can identify prototype pollution patterns in dependency trees and source code during CI/CD pipelines.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #900

Related Articles

high

How SQL Injection happens in Python BigQuery connectors and how to fix it

A high-severity SQL injection vulnerability was discovered in a BigQuery connector's query-building logic, where Python f-strings interpolated user-controlled identifiers—project_id, dataset_id, table_id, and timestamp_column—directly into SQL without validation. An attacker with control over connector configuration could inject arbitrary BigQuery SQL, including destructive statements. The fix introduces strict allowlist-based identifier validation using compiled regular expressions before any S

critical

How Arbitrary Code Execution via Protobuf Definition Injection Happens in Node.js and How to Fix It

A critical vulnerability in protobufjs (CVE-2026-41242) allowed attackers to execute arbitrary code by injecting malicious type fields into protobuf definitions. This fix upgrades the protobufjs dependency from version 7.3.0 to 7.6.5, eliminating the attack vector in a private Node.js application's dependency tree.

high

How Information Disclosure and DoS via malformed Cache-Control directives happens in Node.js undici and how to fix it

A high-severity vulnerability (CVE-2026-13697) in the undici HTTP client library allowed attackers to trigger information disclosure and denial of service through malformed Cache-Control directives. The @jackwener/opencli project upgraded undici from version 7.24.6 to 7.29.0, eliminating the vulnerability in their dependency chain and protecting downstream consumers from exploitation.

high

How Server-Side Request Forgery (SSRF) happens in Python Flask and how to fix it

A high-severity Server-Side Request Forgery (SSRF) vulnerability was discovered in `webui/backend/main.py` at line 6597 of the Posterizarr project. User-controlled `request.media_type` was interpolated directly into a URL used for server-side HTTP requests to The Movie Database (TMDB) API, allowing attackers to manipulate the destination of outbound requests. The fix introduces a strict allowlist that only permits `"movie"` or `"tv"` as valid media types.

high

How Quadratic CPU Consumption Happens in JavaScript YAML Parsing and How to Fix It

A high-severity denial-of-service vulnerability in js-yaml (GHSA-5p4m-2wfm-xmqj) allowed attackers to trigger quadratic CPU consumption by supplying crafted YAML input containing `!!omap` (ordered map) types. The vulnerability affected both the 3.x and 4.x branches of js-yaml, and the fix for CVE-2026-59870 had not been backported to all affected versions. Upgrading from `js-yaml@4.3.0` to `4.3.1` (and `3.15.0` to `3.15.1`) resolves the issue by correcting the inefficient duplicate-key detection

high

How Unicode Hostname Canonicalization Bypass happens in Node.js and how to fix it

CVE-2026-13676 is a high-severity vulnerability in the `fast-uri` npm package where improper Unicode hostname canonicalization allowed attackers to bypass security policies by crafting hostnames that appeared safe but resolved differently after normalization. The fix upgrades `fast-uri` from version 3.1.2 to 4.1.2 and pins the version using an npm `overrides` directive in `package.json` to ensure no transitive dependency pulls in the vulnerable version.