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 Plaintext Credential Storage happens in JSON Configuration Files and how to fix it

A critical security issue was discovered in `assets/settings/global.json` where a real phone number (PII) was stored in plaintext alongside placeholder patterns for API keys and payment credentials. This design encouraged developers to substitute real credentials directly into a version-controlled file, creating a high risk of credential exposure via repository access or filesystem reads. The fix replaces the hardcoded phone number with a placeholder and reinforces safe configuration patterns.

high

How Quadratic CPU Consumption Vulnerabilities Happen in JavaScript YAML Parsers and How to Fix Them

A high-severity denial-of-service vulnerability in js-yaml versions 3.x and 4.x allowed attackers to trigger quadratic CPU consumption through specially crafted YAML documents using the !!omap tag. This fix upgrades js-yaml from 4.1.1 to 4.3.1 and from 3.14.2 to 3.15.1, eliminating the algorithmic complexity attack vector that could freeze Node.js applications processing untrusted YAML input.

high

How javascript.lang.security.detect-child-process.detect-child-process happens in Node.js and how to fix it

A high-severity command injection vulnerability was discovered in `scripts/build.js` where `execSync` was called with string-interpolated arguments (`sourceDir` and `outputPath`) inside a shell command. By replacing `execSync` with `spawnSync` using an argument array (no shell), the fix eliminates the possibility of shell metacharacter injection while preserving identical build behavior.

high

How Command Injection happens in Node.js child_process and how to fix it

A command injection vulnerability in nix.js's Release class allowed potentially malicious input through the `arch` parameter to be executed via shell commands. The fix replaced `execSync()` with `execFileSync()`, eliminating shell interpretation and preventing command injection by passing arguments as an array instead of a concatenated string.

critical

How Sensitive Data Exposure in Error Logging happens in TypeScript/Deno and how to fix it

A critical vulnerability in Supabase Edge Functions allowed sensitive authentication errors and API credentials to leak through verbose error logging. The `cancel-subscription/index.ts` function logged full error objects to the console, potentially exposing Paddle API keys and auth tokens in deployment logs. The fix sanitizes all error messages to log only safe error text while preserving debugging capability.

critical

How HTTP Header Injection Happens in Go and How to Fix It

A critical vulnerability in the file upload handler allowed attackers to inject CRLF sequences into HTTP response headers through crafted filenames. The fix sanitizes user-supplied filenames before using them in Content-Disposition headers, preventing header injection attacks that could lead to cache poisoning, session fixation, or XSS.