Back to Blog
high SEVERITY8 min read

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.

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

CVE-2026-35209 is a prototype pollution vulnerability (CWE-1321) in the `defu` JavaScript library affecting version 6.1.4 and earlier, where passing a `__proto__` key in the defaults argument poisons the global `Object.prototype`, potentially enabling property injection, privilege escalation, or denial of service across the entire Node.js process. The fix is to upgrade `defu` to 6.1.5 and add a pnpm workspace override (`defu: "6.1.5"`) so that all transitive dependents — including `h3` and `unstorage` — resolve the patched version.

Vulnerability at a Glance

cweCWE-1321
fixUpgrade defu to 6.1.5 and pin the version via pnpm workspace overrides in pnpm-lock.yaml and pnpm-workspace.yaml
riskAttacker-controlled input can corrupt Object.prototype, enabling property injection, DoS, or privilege escalation across the entire Node.js process
languageJavaScript / TypeScript (Node.js)
root causedefu 6.1.4 does not sanitize `__proto__` keys when merging defaults into a target object
vulnerabilityPrototype Pollution

How Prototype Pollution Happens in JavaScript via defu and How to Fix It

Introduction

The pnpm-lock.yaml file is easy to overlook — it's auto-generated, rarely read by hand, and almost never the first place developers look for security issues. Yet it was exactly here that Trivy flagged CVE-2026-35209: a high-severity prototype pollution vulnerability in defu, a popular JavaScript utility used for deep-merging default values into objects.

defu is a transitive dependency in many modern JavaScript stacks. In this project, it flows in through h3 (the HTTP framework underpinning Nitro and Nuxt) and unstorage, both of which call defu to merge configuration and request defaults. Because these are production dependencies — not dev-only tooling — the vulnerability was reachable in a live environment.

The root cause: defu 6.1.4 did not sanitize __proto__ keys when recursively merging a defaults object into a target. Passing { "__proto__": { "isAdmin": true } } as a defaults argument would silently walk up the prototype chain and inject isAdmin onto Object.prototype itself — making every plain object in the Node.js process suddenly report isAdmin === true.


The Vulnerability Explained

What is Prototype Pollution?

Every JavaScript object inherits from Object.prototype. When a merge utility blindly iterates over the keys of an input object and writes them to a target, a specially crafted key like __proto__ doesn't create a property named __proto__ — it traverses the prototype chain and writes to the prototype of the target's constructor. If the target is a plain object ({}), that prototype is Object.prototype itself.

// Conceptual illustration of the vulnerable merge pattern in defu 6.1.4
function merge(target, defaults) {
  for (const key in defaults) {
    if (typeof defaults[key] === 'object') {
      merge(target[key], defaults[key]); // ← __proto__ key traverses prototype chain
    } else {
      target[key] = defaults[key];
    }
  }
}

// Attacker-supplied payload
merge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));

// Every plain object now has isAdmin === true
console.log({}.isAdmin); // true  ← prototype has been polluted

The vulnerable version locked in pnpm-lock.yaml was:

# BEFORE (vulnerable)
defu@6.1.4:
  resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}

Attack Scenario Specific to This Codebase

Consider how h3 uses defu internally: when processing an incoming HTTP request, h3 merges route-level defaults with request-level options. If any part of that merge path accepts attacker-influenced data — such as a JSON request body, query parameters parsed into an object, or externally loaded configuration — an attacker could craft a payload like:

{
  "__proto__": {
    "admin": true,
    "bypassRateLimit": true
  }
}

Once defu processes this through its recursive merge, Object.prototype.admin becomes true for the entire lifetime of the Node.js process. Subsequent authorization checks of the form if (user.admin) would pass for every user, even unauthenticated ones, because {}.admin now resolves to true via the prototype chain.

Similarly, unstorage uses defu to merge storage driver options. A polluted prototype could alter the behavior of storage operations across all drivers in the same process.

Real-World Impact

  • Privilege escalation: Authorization checks relying on property existence (obj.isAdmin, obj.role) can be bypassed.
  • Denial of service: Injecting properties that conflict with internal Node.js or framework internals can crash the process.
  • Logic tampering: Feature flags, configuration values, and behavioral switches can be overridden globally.
  • Severity: HIGH — the vulnerability is reachable through production HTTP request handling paths.

The Fix

The fix involves three coordinated changes across two files.

1. Upgrade the resolved version in pnpm-lock.yaml

# BEFORE
defu@6.1.4:
  resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}

# AFTER
defu@6.1.5:
  resolution: {integrity: sha512-pwdBJxJuJXmqrLO6s0VBmfbRz+G7FUzkjldAsdi9Yrv86mPyzq0ll1o8+8gB4Gsr6GJHbK1Lh3ngllgTInDCjA==}

Changing the integrity hash is not cosmetic — it cryptographically pins the download to the patched release. Any tampering with the package would cause pnpm to reject the install.

2. Add a global override in pnpm-lock.yaml

# Added at the top of pnpm-lock.yaml
overrides:
  defu: 6.1.5

This is critical. Without this override, pnpm might resolve defu@6.1.4 for transitive dependents (h3, unstorage) that declare a semver range like ^6.1.4, which technically satisfies 6.1.4. The override forces every package in the dependency tree to use 6.1.5, regardless of what their individual package.json specifies.

3. Add the override to pnpm-workspace.yaml

# pnpm-workspace.yaml (added)
overrides:
  defu: "6.1.5"

This ensures the override is respected at the workspace level, covering all packages in a monorepo setup. Without this, workspace packages that install independently could still resolve the vulnerable version.

Snapshot update

The dependency snapshot entry was also updated:

# BEFORE
defu@6.1.4: {}

# AFTER
defu@6.1.5: {}

And the unstorage snapshot's resolved dependency was updated accordingly:

# BEFORE (in unstorage snapshot)
defu: 6.1.4

# AFTER
defu: 6.1.5

Why 6.1.5 Fixes the Problem

defu 6.1.5 adds explicit key sanitization during recursive merging. Keys equal to __proto__, constructor, and prototype are now skipped, preventing the prototype chain traversal that made the attack possible. The fix is minimal, backward-compatible, and does not change the public API.


Prevention & Best Practices

1. Always sanitize merge keys in custom utilities

If you write your own object-merging logic, explicitly block dangerous keys:

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

function safeMerge(target, source) {
  for (const key of Object.keys(source)) {
    if (DANGEROUS_KEYS.has(key)) continue; // ← block prototype pollution
    if (typeof source[key] === 'object' && source[key] !== null) {
      target[key] = safeMerge(target[key] ?? {}, source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

2. Use Object.create(null) for data objects

When building objects that will hold user-supplied data, use Object.create(null) to create prototype-less objects. These cannot be used as a pollution vector because they have no __proto__:

const safeStore = Object.create(null);
safeStore.__proto__ = "attack"; // just sets a string key, doesn't touch prototype

3. Pin transitive dependencies with lockfile overrides

As demonstrated in this fix, pnpm's overrides field (and npm's overrides / yarn's resolutions) lets you force a specific version of a transitive dependency across the entire tree. Use this pattern whenever a CVE is published for a package you don't directly control:

# pnpm-workspace.yaml
overrides:
  defu: "6.1.5"

4. Freeze Object.prototype in sensitive environments

For high-security applications, consider freezing the prototype at startup:

Object.freeze(Object.prototype);

This prevents any code from modifying Object.prototype at runtime, turning a silent pollution into a thrown TypeError.

5. Scan your lockfile, not just your direct dependencies

Trivy flagged this vulnerability in pnpm-lock.yaml — not in package.json. Many teams scan only their direct dependencies. Transitive dependency scanning is essential because vulnerabilities frequently arrive through indirect packages like defu that you never explicitly installed.

Standards References

  • CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
  • OWASP A08:2021: Software and Data Integrity Failures
  • OWASP A06:2021: Vulnerable and Outdated Components

Key Takeaways

  • defu 6.1.4's recursive merge did not block __proto__ keys, making it possible to poison Object.prototype through any code path that passes attacker-influenced data to defu's defaults argument.
  • The pnpm-lock.yaml integrity hash change is security-critical — it cryptographically pins the download to the patched 6.1.5 release and prevents silent rollback to the vulnerable version.
  • A lockfile override alone is not enough — the fix required coordinated changes to both pnpm-lock.yaml (the overrides block) and pnpm-workspace.yaml to ensure all transitive consumers (h3, unstorage) resolve the patched version.
  • Prototype pollution in a shared HTTP framework dependency like h3 is particularly dangerous because the polluted prototype persists for the entire process lifetime and affects all requests handled after the attack.
  • Lockfile-level scanning (Trivy) caught this where application-level scanning would have missed it — the vulnerability was in a transitive dependency not visible in package.json.

How Orbis AppSec Detected This

  • Source: The __proto__ key in a user-influenced object passed as the defaults argument to defu's merge function.
  • Sink: defu's recursive object merger in version 6.1.4, called internally by h3 (HTTP request handling) and unstorage (storage driver configuration) — both production dependencies resolved in pnpm-lock.yaml.
  • Missing control: No sanitization of __proto__, constructor, or prototype keys before recursive property assignment in defu@6.1.4.
  • CWE: CWE-1321 — Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution').
  • Fix: Upgraded defu from 6.1.4 to 6.1.5 and added workspace-level overrides in both pnpm-lock.yaml and pnpm-workspace.yaml to force all transitive dependents to resolve the patched version.

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-2026-35209 is a textbook example of why transitive dependency security matters as much as direct dependency security. The defu library — a small, focused utility for merging default values — introduced a high-severity prototype pollution vulnerability that could have allowed attackers to corrupt Object.prototype across an entire Node.js process, bypassing authorization checks and altering application behavior globally.

The fix was precise and surgical: upgrade defu to 6.1.5, add lockfile overrides to force the patched version for all transitive consumers, and verify the cryptographic integrity hash. No application logic changed. The attack surface was closed.

The broader lesson is this: your application's security posture is only as strong as your deepest transitive dependency. Scanning lockfiles, enforcing version overrides, and automating CVE remediation are not optional hygiene — they are essential practices for production JavaScript applications.


References

Frequently Asked Questions

What is prototype pollution?

Prototype pollution is an attack where an adversary injects properties into JavaScript's shared `Object.prototype`, causing those properties to appear on every object in the process and potentially altering application logic, bypassing security checks, or crashing the server.

How do you prevent prototype pollution in JavaScript?

Use libraries that explicitly block `__proto__`, `constructor`, and `prototype` keys during object merging; apply `Object.freeze(Object.prototype)` in security-sensitive environments; and pin dependency versions to patched releases using lockfile overrides.

What CWE is prototype pollution?

Prototype pollution maps to CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution').

Is input validation alone enough to prevent prototype pollution?

Not always. Input validation at the API boundary helps, but if an internal utility like `defu` is called with data that was already parsed (e.g., from JSON), the dangerous key may arrive as a plain JavaScript string. Patching the library itself is the most reliable defense.

Can static analysis detect prototype pollution?

Yes. Tools like Trivy (which flagged this CVE), Semgrep, and Snyk can identify known-vulnerable library versions and dangerous merge patterns involving `__proto__` keys in dependency graphs.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #814

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

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

high

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

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