Back to Blog
high SEVERITY6 min read

How prototype pollution via `__proto__` key happens in Node.js defu and how to fix it

A high-severity prototype pollution vulnerability (CVE-2026-35209) was discovered in the `defu` package version 6.1.4, which allowed attackers to inject properties into JavaScript's `Object.prototype` via the `__proto__` key in defaults arguments. The fix upgrades `defu` to version 6.1.5 in the frontend's dependency tree, protecting downstream consumers like `c12` and `dotenv` configuration loaders from malicious property injection.

O
By Orbis AppSec
Published July 12, 2026Reviewed July 12, 2026

Answer Summary

CVE-2026-35209 is a prototype pollution vulnerability in the Node.js `defu` library (versions ≤6.1.4) classified under CWE-1321 (Improperly Controlled Modification of Object Prototype Attributes). The `defu` function fails to sanitize `__proto__` keys when merging defaults objects, allowing attackers who control input files or CLI arguments to pollute `Object.prototype`. The fix is upgrading `defu` to version 6.1.5, which adds explicit `__proto__` key filtering during object merging.

Vulnerability at a Glance

cweCWE-1321
fixUpgrade defu from 6.1.4 to 6.1.5 which sanitizes dangerous keys during merge
riskObject prototype pollution enabling property injection, potential RCE in downstream code
languageJavaScript/Node.js
root causedefu 6.1.4 does not filter `__proto__` keys when recursively merging default objects
vulnerabilityPrototype Pollution via `__proto__` key in defaults argument

How prototype pollution via __proto__ key happens in Node.js defu and how to fix it

Introduction

In a frontend production application, the Trivy security scanner flagged a high-severity prototype pollution vulnerability (CVE-2026-35209) in defu version 6.1.4, a widely-used deep defaults merging utility in the Node.js ecosystem. The vulnerable package was pinned in frontend/pnpm-lock.yaml and consumed by critical configuration tooling—specifically c12 (a configuration loader) and the dotenv/config resolution pipeline.

The defu library's core purpose is deceptively simple: merge default values into objects without overwriting existing properties. But when that merging logic fails to reject the __proto__ key, an attacker who controls any input that flows through defu's merge path can inject properties onto every JavaScript object in the process.

This matters particularly here because the threat model identifies this as a local CLI tool where exploitation requires controlling command-line arguments or input files—a realistic scenario for supply chain attacks, malicious configuration files, or compromised development environments.

The Vulnerability Explained

What defu Does

The defu function recursively merges objects, applying "defaults" to a target. For example:

import { defu } from 'defu';

const config = defu(userConfig, {
  port: 3000,
  host: 'localhost'
});

If userConfig is { port: 8080 }, the result is { port: 8080, host: 'localhost' }. The user's values take precedence; defaults fill in the gaps.

The Flaw in 6.1.4

In defu@6.1.4, the recursive merge operation did not filter the __proto__ key when iterating over the defaults argument's properties. This means if an attacker could supply a defaults object like:

const maliciousDefaults = JSON.parse('{"__proto__": {"isAdmin": true}}');

And this object was passed through defu:

const result = defu({}, maliciousDefaults);

The merge would traverse into __proto__ and assign isAdmin: true to Object.prototype. After this pollution:

const anyObject = {};
console.log(anyObject.isAdmin); // true — every object is now "admin"

Attack Scenario Specific to This Codebase

Looking at the lockfile diff, defu@6.1.4 was consumed by two packages:

  1. c12 (configuration loader, version 3.3.3) — This library loads configuration from files (.config files, nuxt.config.ts, etc.) and merges them using defu.
  2. A config resolution module using confbox, dotenv, and defu together.

The attack path:

  1. An attacker crafts a malicious configuration file (e.g., a JSON or YAML config) containing a __proto__ key with injected properties.
  2. The c12 configuration loader reads this file and passes it to defu as a defaults argument.
  3. defu@6.1.4 recursively merges the __proto__ key into Object.prototype.
  4. Downstream code that checks for properties like isAdmin, allowUnsafe, or debug on plain objects now finds attacker-controlled values.

In a CLI tool context, this could mean:
- Bypassing security checks in build pipelines
- Enabling debug/unsafe modes that expose secrets
- Causing denial of service through type confusion

The Fix

The fix is surgical: upgrade defu from 6.1.4 to 6.1.5, which adds explicit filtering of the __proto__ key during recursive merging.

Changes Made

frontend/package.json — Added explicit defu dependency pinned to the safe version:

     "cmdk": "^1.1.1",
     "codemirror": "^6.0.2",
     "date-fns": "^4.1.0",
+    "defu": "6.1.5",
     "dotenv": "^17.2.3",

By adding defu as a direct dependency at version 6.1.5 (without a caret ^), the lockfile resolution is forced to use the patched version rather than the vulnerable 6.1.4 that transitive dependencies might resolve to.

frontend/pnpm-lock.yaml — The lockfile updates confirm the resolution change:

-  defu@6.1.4:
-    resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
+  defu@6.1.5:
+    resolution: {integrity: sha512-pwdBJxJuJXmqrLO6s0VBmfbRz+G7FUzkjldAsdi9Yrv86mPyzq0ll1o8+8gB4Gsr6GJHbK1Lh3ngllgTInDCjA==}

And critically, the consumers now resolve to 6.1.5:

     dependencies:
       c12: 3.3.3
       consola: 3.4.2
-      defu: 6.1.4
+      defu: 6.1.5
       destr: 2.0.5
     dependencies:
       chokidar: 5.0.0
       confbox: 0.2.4
-      defu: 6.1.4
+      defu: 6.1.5
       dotenv: 17.2.4

Why This Works

Version 6.1.5 of defu adds a check that skips the __proto__ key (and likely constructor.prototype) during the recursive merge loop. This means even if a malicious object containing __proto__ is passed as a defaults argument, those dangerous keys are never traversed or assigned, preventing prototype pollution entirely.

Note that defu@6.1.7 also exists in the lockfile and is presumably safe as well, but the specific transitive dependencies (c12, the config module) were pinned to resolve 6.1.4. The direct dependency addition forces pnpm's resolution to prefer 6.1.5 for these consumers.

Prevention & Best Practices

  1. Pin and audit transitive dependencies: Prototype pollution often lurks in transitive dependencies. Use pnpm audit, npm audit, or Trivy to scan lockfiles regularly.

  2. Use Object.create(null) for lookup maps: When creating objects used as dictionaries, Object.create(null) produces objects with no prototype, immune to pollution.

  3. Freeze prototypes in sensitive contexts:
    javascript Object.freeze(Object.prototype);
    This prevents modification but may break poorly-written libraries.

  4. Validate configuration input: Before passing parsed JSON/YAML through merge utilities, strip dangerous keys:
    javascript function sanitize(obj) { if (typeof obj !== 'object' || obj === null) return obj; const { __proto__, constructor, ...safe } = obj; return Object.fromEntries( Object.entries(safe).map(([k, v]) => [k, sanitize(v)]) ); }

  5. Keep merge utilities updated: Libraries like defu, lodash.merge, deepmerge, and defaults-deep have all had prototype pollution CVEs. Subscribe to security advisories.

  6. Use Semgrep or CodeQL rules to detect unsafe object merging patterns in your own code.

Key Takeaways

  • defu@6.1.4's recursive merge did not filter __proto__ keys, allowing any code path that passes attacker-influenced objects through defu to pollute the global prototype.
  • Transitive dependencies are the real attack surface: The vulnerable defu version wasn't a direct dependency—it was pulled in by c12 and config resolution modules, making it invisible without lockfile scanning.
  • Configuration loaders are high-value targets for prototype pollution because they inherently parse and merge untrusted structured data (JSON, YAML, TOML files).
  • Pinning a direct dependency to override transitive resolution (adding "defu": "6.1.5" to package.json) is an effective pnpm/npm strategy to force-patch vulnerable sub-dependencies.
  • CLI tools with file-based input are not safe from exploitation—if an attacker can influence a config file (via supply chain, shared repos, or social engineering), they can trigger prototype pollution.

How Orbis AppSec Detected This

  • Source: Configuration files and CLI input parsed by c12 and confbox-based config loaders, flowing into defu's merge function as the defaults argument.
  • Sink: defu@6.1.4's recursive object merge logic, which assigns properties from the defaults argument—including __proto__—onto the target object's prototype chain.
  • Missing control: No filtering or rejection of the __proto__ key during recursive property enumeration and assignment in the merge loop.
  • CWE: CWE-1321 (Improperly Controlled Modification of Object Prototype Attributes)
  • Fix: Upgraded defu from 6.1.4 to 6.1.5, which adds explicit __proto__ key filtering during recursive defaults merging.

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 prototype pollution remains one of the most insidious vulnerability classes in the JavaScript ecosystem. The defu library is small, focused, and widely depended upon—exactly the kind of utility where a missing key check can cascade across thousands of projects. By upgrading from 6.1.4 to 6.1.5 and pinning the dependency explicitly, this fix eliminates the attack surface while maintaining full compatibility. If your project uses defu (directly or transitively through Nuxt, c12, unjs packages, or similar tooling), verify your lockfile resolves to ≥6.1.5 immediately.

References

Frequently Asked Questions

What is prototype pollution?

Prototype pollution is a vulnerability where an attacker can inject properties into JavaScript's base Object prototype, causing those properties to appear on all objects in the application, potentially leading to denial of service, authentication bypass, or remote code execution.

How do you prevent prototype pollution in Node.js?

Prevent prototype pollution by using libraries that explicitly filter `__proto__`, `constructor`, and `prototype` keys during object merging, using `Object.create(null)` for lookup maps, freezing prototypes with `Object.freeze(Object.prototype)`, and keeping dependencies updated.

What CWE is prototype pollution?

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

Is input validation enough to prevent prototype pollution?

Input validation alone is insufficient because prototype pollution can occur through deeply nested object merging in utility libraries. You need both input validation and safe merging implementations that explicitly reject dangerous keys like `__proto__`.

Can static analysis detect prototype pollution?

Yes, static analysis tools like Trivy, Snyk, and Semgrep can detect known prototype pollution vulnerabilities in dependencies and flag unsafe object merging patterns in custom code.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #4106

Related Articles

critical

How arbitrary code execution via injected protobuf definition type fields happens in Node.js and how to fix it

A critical arbitrary code execution vulnerability (CVE-2026-41242) was discovered in protobufjs versions prior to 7.5.5 and 8.0.1, allowing attackers to inject malicious code through crafted protobuf definition type fields. The fix upgrades the dependency from the vulnerable version 6.11.4 to 7.5.5, which properly sanitizes type field inputs during protobuf parsing. This vulnerability is especially dangerous because protobufjs is widely used in Node.js applications for serialization, meaning a s

high

How command injection happens in Node.js child_process and how to fix it

A high-severity command injection vulnerability was discovered in `bootstrap.js` at line 34, where the `exec()` function was used to run shell commands constructed from a `folder` variable. By replacing `exec()` with `execFile()` and passing arguments as an array, the fix eliminates the shell interpolation entirely, closing the door on command injection attacks that could have affected all downstream consumers of this Node.js library.

high

How unsafe pickle deserialization happens in NumPy's np.load() and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `tools/ardy-engine/retarget.py` where `np.load()` was called with `allow_pickle=True`, enabling attackers to embed malicious pickle payloads in `.npz` files. The fix was a single-character change—switching `allow_pickle=True` to `allow_pickle=False`—that eliminates the deserialization attack vector while preserving the file's legitimate array data loading functionality.

high

How SSRF and Credential Leakage via Absolute URLs happens in axios and how to fix it

A high-severity vulnerability (CVE-2025-27152) in axios versions prior to 1.8.2 allowed Server-Side Request Forgery (SSRF) attacks and credential leakage when making HTTP requests with absolute URLs. This vulnerability was fixed by upgrading from axios 1.7.4 to 1.8.2 in both package.json and package-lock.json, eliminating the attack vector that could have exposed authentication tokens and enabled unauthorized server-side requests.

high

How pickle-based arbitrary code execution happens in PyTorch and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `scripts/export_joyvasa_audio.py` where `torch.load()` was called with `weights_only=False`, allowing any pickle-serialized Python object — including malicious code — to execute during checkpoint loading. The fix switches to `weights_only=True` and explicitly allowlists only the two non-standard classes the checkpoint actually requires: `argparse.Namespace` and `pathlib.PosixPath`. This closes a real code execution path tha

critical

How WebSocket protocol length header abuse happens in Node.js and how to fix it

A critical vulnerability (CVE-2026-54466) in websocket-driver versions prior to 0.7.5 allows attackers to corrupt WebSocket messages by manipulating protocol length headers. This can lead to data integrity issues, denial of service, or potentially arbitrary code execution in affected Node.js applications. The fix involves upgrading the websocket-driver dependency to version 0.7.5.