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
Published August 26, 2026Reviewed 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.1supports-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.23chalk@^2.4.1supports-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

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.