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 missing Dependabot cooldown happens in GitHub Actions and how to fix it

A high-severity configuration vulnerability was discovered in a `.github/dependabot.yml` file that lacked a cooldown period for package updates. Without this safeguard, Dependabot could immediately propose updates to newly published package versions—including potentially malicious or unstable releases. The fix adds a simple `cooldown` block with a 7-day waiting period before any new package version is suggested.

high

How Server-Sent Events Injection via Unsanitized Newlines happens in Node.js h3 and how to fix it

A high-severity Server-Sent Events (SSE) injection vulnerability (CVE-2026-33128) was discovered in the h3 HTTP framework, where unsanitized newline characters in event stream fields could allow attackers to inject arbitrary SSE messages. The fix upgrades h3 from version 1.15.5 to 1.15.6 in the frontend's dependency tree, ensuring that newline characters are properly sanitized before being written to event streams.

high

How Memory Exhaustion via Large Comma-Separated Selector Lists happens in Python Soup Sieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in Soup Sieve version 2.8.3, affecting Python applications that parse CSS selectors from user-controlled input. The vulnerability allows attackers to craft malicious selector lists that consume excessive memory, potentially causing denial of service. The fix involves upgrading to soupsieve 2.8.4, which implements proper resource limits on selector parsing.

critical

How buffer overflow in memcpy() happens in Node.js N-API bindings and how to fix it

A critical buffer overflow vulnerability was discovered in the GetBufferAsVector() function in examples_nodejs/src/zupt_napi.cpp, where memcpy() copied data from JavaScript Uint8Array buffers without proper bounds validation. This vulnerability could allow attackers to trigger memory corruption by providing maliciously crafted input arrays to the native Node.js module, potentially leading to crashes or arbitrary code execution.

high

How memory exhaustion via large comma-separated selector lists happens in Python soupsieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in soupsieve 2.8.3, a CSS selector library used by BeautifulSoup in Python. An attacker who could influence CSS selector input could craft large comma-separated selector lists to exhaust system memory, causing denial of service. The fix upgrades soupsieve from 2.8.3 to 2.8.4 in the backend's `uv.lock` dependency file.

high

How buffer overflow from unsafe string copy functions happens in C network interface code and how to fix it

A high-severity buffer overflow vulnerability was discovered in `generic/eth-impl.c`, where unsafe `strncpy()` and `sprintf()` calls could write beyond buffer boundaries when handling network interface names and device filenames. The fix replaced these dangerous functions with bounded `snprintf()` calls that guarantee null-termination and prevent memory corruption.