Back to Blog
high SEVERITY8 min read

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

A high-severity prototype pollution vulnerability (CVE-2020-8203) was identified in the lodash library via the `zipObjectDeep` function, present as a transitive dependency through postcss in the project's `yarn.lock`. The fix upgrades postcss from 8.5.8 to 8.5.12 using a Yarn resolution override, eliminating the vulnerable lodash code path and reducing the attack surface against crafted CSS input. This change protects the application from object prototype manipulation that could lead to informat

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published August 26, 2026•Reviewed August 26, 2026

Answer Summary

CVE-2020-8203 is a prototype pollution vulnerability in lodash's `zipObjectDeep` function (CWE-1321), affecting Node.js applications that include lodash as a direct or transitive dependency. An attacker can craft a malicious key (e.g., `__proto__`) to pollute the JavaScript Object prototype, potentially enabling denial of service or property injection across the entire runtime. In this project, the vulnerable version was pulled in transitively through postcss 8.5.8 (and an older postcss 6.x entry). The fix pins postcss to 8.5.12 via a `resolutions` field in `package.json` and updates `yarn.lock` accordingly, removing both the postcss 6.x entry and its chalk/supports-color dependencies that carried the vulnerable lodash version.

Vulnerability at a Glance

cweCWE-1321
fixPin postcss to 8.5.12 via Yarn resolutions, removing the vulnerable postcss 6.x entry and its lodash dependency chain
riskAttacker can pollute Object.prototype, causing DoS or unauthorized property injection across the entire Node.js process
languageJavaScript / Node.js
root causelodash's `zipObjectDeep` function does not sanitize user-controlled keys before deep-assigning them to objects, allowing `__proto__` injection
vulnerabilityPrototype Pollution via lodash zipObjectDeep

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

The Problem Hidden in Your Lock File

The yarn.lock file is easy to overlook — it's auto-generated, rarely read by humans, and usually committed without much scrutiny. But it encodes the exact versions of every dependency, including transitive ones you never explicitly chose. In this project, yarn.lock was carrying two separate entries for postcss: a modern 8.5.8 build and a legacy 6.0.23 build, the latter pulling in a version of chalk and supports-color that depended on a vulnerable version of lodash.

That lodash version contained CVE-2020-8203, a high-severity prototype pollution flaw in the zipObjectDeep function. The fix — upgrading postcss to 8.5.12 and removing the stale v6 entry — is surgical and precise, but understanding why it was necessary requires a look at what prototype pollution actually does at runtime.


The Vulnerability Explained

What Is Prototype Pollution?

In JavaScript, every object inherits from Object.prototype. If an attacker can control the key used in a deep-assignment operation, they can write to __proto__ and inject properties onto the shared prototype. Because all objects share this prototype, the injected property becomes visible on every plain object in the Node.js process — not just the one being manipulated.

Lodash's zipObjectDeep function takes two arrays — one of paths, one of values — and builds a deeply nested object:

// Vulnerable usage pattern in lodash < 4.17.19
_.zipObjectDeep(['a.b.c'], [1]);
// Returns: { a: { b: { c: 1 } } }

// Attacker-controlled input:
_.zipObjectDeep(['__proto__.polluted'], [true]);
// Now: ({}).polluted === true  — on EVERY object in the process

The zipObjectDeep function did not validate whether path segments contained __proto__ or constructor before performing the deep assignment. This allowed a crafted path string to escape the intended object and write directly to the global prototype chain.

How This Reached the Project

The vulnerable lodash version entered the dependency tree through postcss 6.0.23, which was still present in yarn.lock alongside the newer postcss 8.x:

# Before the fix — yarn.lock contained TWO postcss entries:

"postcss@npm:^6.0.1, postcss@npm:^6.0.2":
  version: 6.0.23
  resolution: "postcss@npm:6.0.23"
  dependencies:
    chalk: "npm:^2.4.1"        # <-- pulls supports-color, which pulls lodash
    source-map: "npm:^0.6.1"
    supports-color: "npm:^5.4.0"

"postcss@npm:^8.4.40":
  version: 8.5.8
  resolution: "postcss@npm:8.5.8"
  dependencies:
    nanoid: "npm:^3.3.11"
    picocolors: "npm:^1.1.1"
    source-map-js: "npm:^1.2.1"

The chalk@npm:^2.4.1 dependency in the postcss 6.x entry was the gateway: that version of chalk depended on supports-color@^5.4.0, which in turn carried the vulnerable lodash. The postcss 8.x entry had already modernized its dependency chain (using picocolors instead of chalk), but the stale v6 entry kept the old chain alive.

Real-World Impact for This Application

In the context of a web application using postcss to process CSS (for example, in a webpack build pipeline or a server-side CSS-in-JS renderer), an attacker who can influence CSS input could potentially:

  1. Trigger denial of service by crafting CSS that causes postcss's parsing logic to invoke the vulnerable lodash path with attacker-controlled keys, polluting Object.prototype and breaking assumptions in other parts of the application.
  2. Inject properties into the prototype chain, causing security checks that rely on property existence (e.g., if (obj.isAdmin)) to return unexpected truthy values.
  3. Exploit downstream logic — once Object.prototype is polluted, any code path that reads properties from plain objects without explicit hasOwnProperty guards becomes a potential vector.

The scanner assessment notes the vulnerability is "present in dependency tree, not confirmed reachable," but the presence of the vulnerable code in the resolved dependency graph is sufficient risk to warrant immediate remediation.


The Fix

Two Files, Three Changes

The fix touches package.json and yarn.lock. Here's exactly what changed and why each change was necessary.

1. package.json — Pinning via Yarn Resolutions

// Before
{
  "packageManager": "yarn@4.17.1"
}

// After
{
  "packageManager": "yarn@4.17.1",
  "resolutions": {
    "postcss": "8.5.12"
  }
}

The resolutions field is a Yarn-specific mechanism that forces all packages in the dependency tree — regardless of what version they request — to resolve to the specified version. Without this, even if you upgrade your direct postcss dependency, a transitive dependency that requests postcss@^6.0.1 would still resolve to 6.x. The resolution override breaks that pattern entirely.

2. yarn.lock — Removing the postcss 6.x Entry

-"postcss@npm:^6.0.1, postcss@npm:^6.0.2":
-  version: 6.0.23
-  resolution: "postcss@npm:6.0.23"
-  dependencies:
-    chalk: "npm:^2.4.1"
-    source-map: "npm:^0.6.1"
-    supports-color: "npm:^5.4.0"
-  checksum: 10/218e21b4f42b2147b03c1a8898271629c9fd5b53a08...
-  languageName: node
-  linkType: hard

-"postcss@npm:^8.4.40":
-  version: 8.5.8
-  resolution: "postcss@npm:8.5.8"
+  "postcss@npm:8.5.12":
+  version: 8.5.12
+  resolution: "postcss@npm:8.5.12"
   dependencies:
     nanoid: "npm:^3.3.11"
     picocolors: "npm:^1.1.1"
     source-map-js: "npm:^1.2.1"
-  checksum: 10/cbacbfd7f767e2c820d4bf09a3a744834dd7d14f69ff...
+  checksum: 10/ec6b79b68c363eca3c8ffceb134a4ab637274aee6ac...

The lock file now contains a single postcss entry, pinned to 8.5.12. The v6 entry — and with it the chalk@^2.4.1 → supports-color@^5.4.0 → vulnerable lodash chain — is completely gone.

3. Cascade Cleanup — chalk and supports-color

-"chalk@npm:^2.3.2, chalk@npm:^2.4.1, chalk@npm:^2.4.2":
+"chalk@npm:^2.3.2, chalk@npm:^2.4.2":
   version: 2.4.2

-"supports-color@npm:^5.3.0, supports-color@npm:^5.4.0":
+"supports-color@npm:^5.3.0":
   version: 5.5.0

Because the postcss 6.x entry was the only consumer of chalk@^2.4.1 and supports-color@^5.4.0, removing it also cleans up those specifier aliases from the lock file. This is a meaningful reduction in attack surface: the lock file no longer records resolution paths for dependency ranges that were solely serving the vulnerable subtree.


Key Takeaways

  • The postcss 6.0.23 entry in yarn.lock was the root of the problem — not postcss itself, but the legacy chalk/supports-color dependency chain it carried, which resolved to a vulnerable lodash version.
  • Yarn's resolutions field is a surgical tool for forcing all consumers of a package to use a safe version, regardless of what semver range they specify — use it when transitive dependencies can't be updated through normal channels.
  • Two postcss major versions coexisting in yarn.lock (^6.0.x and ^8.4.x) doubled the attack surface; the fix reduces this to a single, pinned, patched entry.
  • zipObjectDeep with user-controlled paths is inherently dangerous — any code path that allows external input to drive deep object key assignment should be treated as a high-risk pattern.
  • Lock file hygiene matters for security — regularly audit yarn.lock or package-lock.json for stale major-version entries that may carry vulnerability chains the primary dependency has long since abandoned.

How Orbis AppSec Detected This

  • Source: Crafted CSS input processed by the postcss pipeline, where user-influenced content can propagate into lodash utility functions via the postcss 6.x dependency chain.
  • Sink: lodash.zipObjectDeep() performing deep property assignment with unsanitized path keys, reachable through the postcss@6.0.23 → chalk@^2.4.1 → supports-color@^5.4.0 dependency path recorded in yarn.lock.
  • Missing control: No key sanitization to block __proto__, constructor, or prototype segments before deep object assignment; no resolution pin to prevent the vulnerable postcss 6.x subtree from being installed.
  • CWE: CWE-1321 — Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution').
  • Fix: Added a resolutions entry in package.json pinning postcss to 8.5.12 and regenerated yarn.lock to remove the vulnerable postcss@6.0.23 entry and its dependency chain entirely.

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

This fix is a good example of how transitive dependency hygiene directly affects application security. The project's own code never called zipObjectDeep — but the vulnerability was still present in the resolved dependency graph, reachable through a stale postcss 6.x entry that had quietly persisted in yarn.lock alongside the modern 8.x version.

The remediation is precise: a resolutions pin in package.json forces the entire dependency tree to use postcss 8.5.12, the lock file is regenerated to reflect a single clean entry, and the legacy chalk/supports-color chain that carried the lodash vulnerability is removed entirely. No application logic changed; only the dependency resolution did.

For developers: treat your lock file as a security artifact. Review it for duplicate major versions, run scanners against it in CI, and don't hesitate to use resolutions or overrides to enforce safe versions across your entire transitive dependency tree.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #668

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