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

high

How Octal IP Address Parsing Inconsistency Enables SSRF in Node.js and How to Fix It

A critical parsing inconsistency in the `ip-address` npm package (version 10.2.0) allowed attackers to bypass SSRF protections by exploiting how leading-zero octets are interpreted differently—decimal by the library versus octal by system resolvers. This vulnerability (CVE-2026-69192) was fixed by upgrading to version 10.3.1 using an npm override, ensuring consistent IP address validation across the application.

high

How Command Injection Happens in Node.js Child Process Calls and How to Fix It

A Node.js library was vulnerable to command injection through unsafe use of `execSync()` with shell string interpolation in the `index.js` file. By switching to `execFileSync()` with argument arrays, the fix eliminates the ability for attackers to inject shell metacharacters through file paths. This change demonstrates a critical security hardening pattern for any Node.js code that spawns child processes.

high

How NO_PROXY bypass via crafted URL happens in Node.js axios and how to fix it

A high-severity vulnerability (CVE-2026-42043) in the axios HTTP client library allowed attackers to bypass NO_PROXY environment variable restrictions using specially crafted URLs. This could route sensitive internal traffic through attacker-controlled proxy servers. The fix upgrades axios from 1.13.6 to 1.18.0, which includes a rewritten proxy resolution mechanism using `proxy-from-env` v2.1.0 and the `https-proxy-agent` package.

high

How Denial of Service via Infinite Loop happens in Node.js dependencies and how to fix it

A high-severity vulnerability in the nanoid package (CVE-2026-67213) allowed attackers to trigger infinite loops through the customAlphabet function, potentially causing complete denial of service. This fix upgrades nanoid from version 3.3.16 to 3.3.17 in the app_store dependency tree, eliminating the DoS risk through a simple version override.

high

How Denial of Service via Deeply Nested Field Names Happens in Node.js Multer and How to Fix It

A high-severity Denial of Service vulnerability (CVE-2026-5079) was discovered in the multer package, a popular Node.js middleware for handling multipart form data. Attackers could craft malicious requests with deeply nested field names to exhaust server resources. The fix upgrades multer from version 2.0.2 to 2.2.0, which implements proper limits on field name parsing depth.

critical

How SQL Injection happens in PHP PDO queries and how to fix it

A critical SQL injection vulnerability was discovered in the `getOfficialContests()` method of ContestRepository.php, where the `$site_id` parameter was directly interpolated into a SQL query string instead of using prepared statements. This vulnerability allowed attackers to inject arbitrary SQL commands and potentially access or manipulate the entire contest database. The fix replaced `pdo->query()` with `pdo->prepare()` and proper parameter binding.