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.


Prevention & Best Practices

1. Use resolutions (Yarn) or overrides (npm) for Transitive Dependencies

When a transitive dependency carries a known vulnerability and you can't immediately update the direct dependency that pulls it in, use your package manager's override mechanism:

// Yarn
"resolutions": { "lodash": ">=4.17.21" }

// npm
"overrides": { "lodash": ">=4.17.21" }

This is a targeted fix that doesn't require waiting for upstream maintainers.

2. Audit Your Lock File for Duplicate Major Versions

Multiple entries for the same package at different major versions (like postcss 6.x and 8.x coexisting) is a red flag. The older version may lack security patches that the newer one includes. Run:

yarn why postcss
# or
npm ls postcss

to visualize which packages are pulling in each version.

3. Validate Keys Before Deep Object Assignment

If you write any code that performs deep property assignment based on user-controlled input, always sanitize keys:

// Dangerous
function deepSet(obj, path, value) {
  _.set(obj, path, value); // path from user input — never do this
}

// Safer
function deepSet(obj, path, value) {
  const forbidden = ['__proto__', 'constructor', 'prototype'];
  const parts = path.split('.');
  if (parts.some(p => forbidden.includes(p))) {
    throw new Error('Forbidden key in path');
  }
  _.set(obj, path, value);
}

4. Run Dependency Scanners in CI

Integrate Trivy, Snyk, or npm audit into your CI pipeline so vulnerable transitive dependencies are caught before they reach production:

# Example GitHub Actions step
- name: Run Trivy vulnerability scanner
  uses: aquasecurity/trivy-action@master
  with:
    scan-type: 'fs'
    scan-ref: '.'
    severity: 'HIGH,CRITICAL'

5. Security Standards

  • OWASP A06:2021 – Vulnerable and Outdated Components: This vulnerability is a textbook example of the risk posed by unmonitored transitive dependencies.
  • CWE-1321: Improperly Controlled Modification of Object Prototype Attributes.
  • Keep a Software Bill of Materials (SBOM) to track all direct and transitive dependencies and their versions.

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.


References

Frequently Asked Questions

What is prototype pollution?

Prototype pollution is a JavaScript vulnerability where an attacker manipulates an object's `__proto__` property to inject or override properties on the global Object prototype, affecting all objects in the runtime.

How do you prevent prototype pollution in Node.js?

Sanitize user-controlled keys before using them in deep-merge or object-assignment functions, use `Object.create(null)` for dictionaries, keep dependencies up to date, and use tools like Snyk or Trivy to detect vulnerable transitive dependencies.

What CWE is prototype pollution?

Prototype pollution is classified as CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution').

Is input validation alone enough to prevent prototype pollution?

Not always. Input validation helps, but the safest approach is to use patched library versions that internally block `__proto__` and `constructor` keys, combined with dependency pinning via `resolutions` or `overrides`.

Can static analysis detect prototype pollution?

Yes. Tools like Trivy, Snyk, and Semgrep can identify vulnerable dependency versions and flag dangerous patterns like unguarded deep-assignment functions in JavaScript codebases.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #668

Related Articles

critical

How Server-Side Template Injection happens in Node.js EJS and how to fix it

CVE-2022-29078 is a critical server-side template injection (SSTI) vulnerability in EJS versions prior to 3.1.7, where the `outputFunctionName` option is passed directly into generated code without sanitization, allowing attackers to execute arbitrary JavaScript on the server. The fix upgrades the EJS dependency from 2.7.4 to 3.1.7+ (resolved here as 6.0.1), eliminating the unsafe code generation path. Any Node.js application rendering EJS templates with user-influenced options is at risk of ful

high

How Prototype Pollution happens in JavaScript via defu and how to fix it

CVE-2026-35209 is a high-severity prototype pollution vulnerability in the `defu` JavaScript library (versions prior to 6.1.5), where a crafted `__proto__` key in the defaults argument can corrupt the global Object prototype. The fix upgrades `defu` from 6.1.4 to 6.1.5 in `pnpm-lock.yaml` and enforces the version via a workspace override, closing the attack surface in production code that depends on `defu` for deep object merging.

critical

How eval() Code Injection happens in JavaScript and how to fix it

A critical code injection vulnerability was discovered in `js/lib/jsencrypt.js` at line 195, where a direct `eval()` call executed a JavaScript string shim for the `process` object in browser environments. If an attacker could influence the string passed to `eval()`—through a compromised dependency, a man-in-the-middle attack, or supply chain tampering—they could achieve arbitrary JavaScript execution in any user's browser. The fix replaces the `eval()` call with the equivalent inline JavaScript

high

How Unsafe eval() in JavaScript Happens in React Components and How to Fix It

A high-severity code injection vulnerability was discovered in `TurnPlanner.tsx`, where the `parseInputExpr` function used JavaScript's `Function` constructor — effectively `eval()` — to evaluate user-provided mathematical expressions. The regex guard in place only checked for the presence of arithmetic operators, not whether the input was safe to execute, leaving the door open for arbitrary JavaScript injection. A targeted whitelist fix was applied to reject any input containing characters outs

critical

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

CVE-2023-36665 is a critical prototype pollution vulnerability in protobufjs that allows attackers to corrupt JavaScript's Object prototype by crafting malicious protobuf messages. The vulnerability existed in protobufjs 6.11.3 and was resolved by upgrading to 6.11.4 (and 7.2.5 for the v7 branch). Applications that parse user-supplied protobuf data are directly at risk of runtime behavior manipulation, privilege escalation, or denial of service.

high

How Dependabot Missing Cooldown happens in GitHub Actions and how to fix it

A high-severity misconfiguration in `.github/dependabot.yml` left this Node.js library without a cooldown period on dependency updates, meaning Dependabot could immediately propose upgrades to newly published — potentially malicious or unstable — package versions. The fix adds a `cooldown` block with `default-days: 7` to both the `npm` and `github-actions` ecosystems, introducing a mandatory waiting period before any newly released version is surfaced as an update candidate. Because this project