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
•Technically reviewed by Anupam Mediratta•Published August 26, 2026•Reviewed 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.


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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #814

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