Back to Blog
high SEVERITY7 min read

Prototype Pollution in defu's Defaults Argument via `__proto__` Key (CVE-2026-35209)

CVE-2026-35209 is a high-severity prototype pollution vulnerability in the `defu` JavaScript library (versions prior to 6.1.5) that allows attackers to inject arbitrary properties onto `Object.prototype` by passing a `__proto__` key in the defaults argument. The vulnerability was present in the `blog-site` project's dependency tree and was resolved by upgrading `defu` to 6.1.5 and adding an explicit `overrides` entry to prevent transitive re-introduction of the vulnerable version.

O
By Orbis AppSec
Published June 1, 2026Reviewed June 3, 2026

Answer Summary

CVE-2026-35209 is a high-severity prototype pollution vulnerability (CWE-1321) in the `defu` JavaScript library affecting versions prior to 6.1.5. An attacker can pass a `__proto__` key in the defaults argument to `defu()`, injecting arbitrary properties onto `Object.prototype` and affecting all objects in the Node.js process. The fix is to upgrade `defu` to version 6.1.5, which sanitizes reserved keys like `__proto__`, and to add a package manager `overrides` entry to prevent vulnerable versions from being re-introduced transitively.

Vulnerability at a Glance

cweCWE-1321
fixUpgrade defu to 6.1.5 and add an `overrides` entry in package.json to block transitive re-introduction of the vulnerable version
riskAttackers can inject properties onto Object.prototype, affecting all objects application-wide, potentially enabling privilege escalation, DoS, or RCE
languageJavaScript (Node.js)
root causedefu's merge logic did not sanitize reserved keys like `__proto__` before assigning properties during defaults resolution
vulnerabilityPrototype Pollution via `__proto__` key in defu defaults argument

Prototype Pollution in defu's Defaults Argument via __proto__ Key (CVE-2026-35209)

Introduction

The blog-site/package-lock.json file in this project pins the dependency tree for a Node.js blog application. Buried inside that tree was defu version 6.1.4 — a popular utility for deep-merging default values into objects. On the surface it looks harmless: you pass it an object and a set of defaults, and it fills in any missing keys. But a subtle flaw in how defu 6.1.4 processes the keys of the defaults argument meant that an attacker who could influence those defaults could inject properties directly onto Object.prototype — a classic prototype pollution attack.

This post walks through exactly what went wrong, how it could be exploited in the context of a blog application, and what the upgrade to defu 6.1.5 does to close the door.


The Vulnerability Explained

What Is Prototype Pollution?

Every JavaScript object inherits properties from Object.prototype. If an attacker can write an arbitrary key to Object.prototype, that key becomes visible on every plain object in the running process — including those used for authentication checks, configuration, and template rendering.

// After a successful prototype pollution attack:
console.log({}.isAdmin);       // true  ← injected by attacker
console.log({}.debugMode);     // true  ← injected by attacker

The consequences range from bypassing authorization logic to crashing the server with a denial-of-service.

How defu 6.1.4 Was Vulnerable

defu is designed to merge a "defaults" object into a target object without overwriting keys that already exist. Internally, it iterates over the keys of the defaults argument and copies missing values. In version 6.1.4, this key iteration did not filter out the special JavaScript key __proto__.

Consider this simplified representation of the vulnerable pattern:

// defu 6.1.4 — vulnerable key-merging logic (simplified)
function applyDefaults(target, defaults) {
  for (const key in defaults) {
    // ❌ No check for '__proto__', 'constructor', or 'prototype'
    if (target[key] === undefined) {
      target[key] = defaults[key];
    }
  }
  return target;
}

When key is __proto__, the assignment target["__proto__"] = defaults["__proto__"] does not set a property named __proto__ on target. Instead, JavaScript's engine interprets it as setting properties on Object.prototype itself — polluting the prototype chain for every object in the process.

An attacker who can supply or influence the defaults argument — for example through a deserialized JSON payload, a query-string parsed into an object, or user-generated front-matter in a blog post — can trigger this:

// Attacker-controlled input parsed from, e.g., a blog post's YAML front-matter
const maliciousDefaults = JSON.parse('{"__proto__": {"isAdmin": true}}');

// Calling defu with the poisoned defaults
const config = defu(userConfig, maliciousDefaults);

// Now EVERY plain object in the process has isAdmin === true
console.log({}.isAdmin); // true — prototype has been polluted

Why This Matters for blog-site

The blog-site project uses gray-matter (for YAML/JSON front-matter parsing) and satori for Open Graph image generation — both of which interact with user-authored content and configuration objects. defu is a transitive dependency used by several tools in this stack for merging configuration defaults.

If blog post front-matter or a configuration file is ever passed through a defu-powered merge path, an attacker who controls that content could pollute the prototype and affect downstream logic — such as authorization middleware, template variable resolution, or server-side rendering checks.


The Fix

What Changed

The fix involves two coordinated changes across package.json and package-lock.json.

blog-site/package.json — Adding an overrides entry:

-  }
+  },
+  "overrides": {
+    "defu": "^6.1.5"
+  }
 }

The overrides field (supported in npm 8.3+) forces all instances of defu in the dependency tree — whether direct or transitive — to resolve to at least version 6.1.5. This is critical because defu is a transitive dependency here; without the override, a parent package could re-introduce 6.1.4.

blog-site/package-lock.json — Pinning the resolved version:

 "node_modules/defu": {
-  "version": "6.1.4",
-  "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
-  "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
+  "version": "6.1.5",
+  "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.5.tgz",
+  "integrity": "sha512-pwdBJxJuJXmqrLO6s0VBmfbRz+G7FUzkjldAsdi9Yrv86mPyzq0ll1o8+8gB4Gsr6GJHbK1Lh3ngllgTInDCjA==",
   "license": "MIT"
 },

The lock file change guarantees reproducible installs: every npm ci run in CI/CD and production will pull exactly 6.1.5 with the verified integrity hash, not the vulnerable 6.1.4.

What defu 6.1.5 Does Differently

Version 6.1.5 adds explicit key sanitization before processing any key from the defaults argument. The fix blocks __proto__, constructor, and prototype from being used as merge keys — the three vectors historically exploited in prototype pollution attacks:

// defu 6.1.5 — safe key-merging logic (conceptual)
const BLOCKED_KEYS = new Set(['__proto__', 'constructor', 'prototype']);

function applyDefaults(target, defaults) {
  for (const key in defaults) {
    if (BLOCKED_KEYS.has(key)) continue; // ✅ Skip dangerous keys
    if (target[key] === undefined) {
      target[key] = defaults[key];
    }
  }
  return target;
}

Even if an attacker supplies {"__proto__": {"isAdmin": true}} as the defaults argument, the key is silently skipped and Object.prototype is never touched.


Prevention & Best Practices

1. Always Sanitize Keys Before Object Merging

Any utility that iterates over object keys and writes them to another object is a potential prototype pollution vector. When writing or auditing such utilities, always block the three dangerous keys:

const DANGEROUS_KEYS = ['__proto__', 'constructor', 'prototype'];

function safeSet(obj, key, value) {
  if (DANGEROUS_KEYS.includes(key)) return; // Never write these
  obj[key] = value;
}

Alternatively, use Object.create(null) to create prototype-free objects when merging untrusted input, so there is no prototype to pollute.

2. Use overrides (npm) or resolutions (Yarn) for Transitive Vulnerabilities

When a vulnerable package is a transitive dependency you don't control directly, use your package manager's override mechanism:

// npm (package.json)
"overrides": { "defu": "^6.1.5" }

// Yarn (package.json)
"resolutions": { "defu": "^6.1.5" }

This ensures the fix propagates even if an intermediate dependency hasn't updated yet.

3. Validate and Sanitize User-Controlled Objects Early

In the blog-site context, front-matter parsed from blog posts (via gray-matter) is user-controlled data. Before that data flows into any merge or defaults operation, validate its shape:

import { defu } from 'defu'; // now safe in 6.1.5
import matter from 'gray-matter';

const { data: frontMatter } = matter(rawMarkdown);

// Still good practice: validate expected keys explicitly
const safeFrontMatter = {
  title: String(frontMatter.title ?? ''),
  date: String(frontMatter.date ?? ''),
  tags: Array.isArray(frontMatter.tags) ? frontMatter.tags : [],
};

const postConfig = defu(safeFrontMatter, defaultPostConfig);

4. Run SCA Scanners in CI

This vulnerability was caught by Trivy (rule CVE-2026-35209). Integrate software composition analysis (SCA) scanning into your CI pipeline so vulnerable transitive dependencies are flagged before they reach production:

# Example GitHub Actions step
- name: Run Trivy vulnerability scanner
  uses: aquasecurity/trivy-action@master
  with:
    scan-type: 'fs'
    scan-ref: '.'
    severity: 'HIGH,CRITICAL'
    exit-code: '1'

5. Relevant Standards and References


Key Takeaways

  • defu 6.1.4's key iteration skipped no dangerous keys — any caller passing {"__proto__": {...}} as the defaults argument could silently pollute Object.prototype for the entire Node.js process.
  • Transitive dependencies are just as dangerous as direct onesdefu wasn't in blog-site's direct dependencies, yet it was reachable and exploitable; the overrides field in package.json was necessary to force the safe version across the whole tree.
  • The lock file integrity hash matters — the updated integrity field in package-lock.json for defu 6.1.5 ensures that even a compromised npm registry mirror cannot substitute the vulnerable version.
  • User-authored content (blog front-matter) is attacker-controlled input — in a blog application, YAML/JSON front-matter flows through parsing and merging pipelines and must be treated with the same suspicion as HTTP request bodies.
  • Key sanitization (__proto__, constructor, prototype) is a non-negotiable requirement for any object-merging utility that accepts external input — not an optional hardening step.

Conclusion

CVE-2026-35209 is a textbook example of why supply-chain security deserves the same attention as first-party code. A single missing key-sanitization check in defu 6.1.4's defaults-merging logic was enough to expose every downstream application — including blog-site — to prototype pollution. The fix is precise: upgrade to defu 6.1.5 (which blocks __proto__ and related keys) and lock that version in place with an overrides entry so no transitive dependency can silently re-introduce the vulnerability.

The broader lesson is that object-merging utilities are high-value targets for prototype pollution because they are explicitly designed to write keys from one object onto another. Whenever you write or adopt such a utility, key sanitization isn't optional — it's the price of admission for handling untrusted input safely.


This security fix was automatically identified and remediated by OrbisAI Security. Automated security scanning and patching helps teams stay ahead of emerging CVEs without manual triage overhead.

Frequently Asked Questions

What is prototype pollution?

Prototype pollution is an attack where an adversary injects properties into JavaScript's `Object.prototype`, causing those properties to appear on every object in the application and potentially altering program logic, bypassing security checks, or enabling code execution.

How do you prevent prototype pollution in JavaScript?

Prevent prototype pollution by validating and sanitizing object keys before merging (blocking `__proto__`, `constructor`, and `prototype`), using `Object.freeze(Object.prototype)`, preferring `Object.create(null)` for plain data maps, and keeping dependency versions up to date.

What CWE is prototype pollution?

Prototype pollution maps to CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution').

Is input validation alone enough to prevent prototype pollution?

Input validation helps but is not sufficient on its own — library-level fixes that sanitize keys during merge operations (as done in defu 6.1.5) are essential, especially when the vulnerability lives in a transitive dependency outside your direct control.

Can static analysis detect prototype pollution?

Yes. Static analysis tools like Semgrep and Snyk can detect patterns where untrusted data flows into object merge operations without key sanitization. Orbis AppSec detected this vulnerability automatically in the blog-site dependency tree.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #3974

Related Articles

high

How Spring Boot EndpointRequest.to() security bypass happens in Java Spring Boot and how to fix it

CVE-2025-22235 is a high-severity vulnerability in Spring Boot where `EndpointRequest.to()` creates an incorrect request matcher when an actuator endpoint is not exposed, potentially allowing unauthorized access to protected endpoints. The fix upgrades Spring Boot from 3.4.4 to 3.4.5 in the anti-corruption-layer service's `pom.xml`. This is particularly dangerous because actuator endpoints can expose sensitive operational data and administrative functions.

high

How heap buffer overflow happens in C JMA archive extraction and how to fix it

A heap buffer overflow vulnerability in `jma/jma.cpp` allowed a crafted JMA ROM archive to trigger out-of-bounds memory writes during file extraction. The flaw existed at line 446, where `memcpy` was called with `first_chunk_offset` and `copy_amount` values derived directly from archive header metadata without any validation that those values stayed within the bounds of either the source or destination buffer. The fix adds a pre-copy bounds check that rejects malformed archives before the danger

high

How path traversal in open() happens in Python and how to fix it

A high-severity path traversal vulnerability was discovered in `tool/update-doc.py`, where user-controlled input was passed directly to Python's `open()` function without sanitization. This flaw could allow an attacker to read arbitrary files on the server by manipulating the file path. The fix ensures that file paths are validated and restricted to an intended directory before being opened.

high

NULL Pointer Dereference in ESP8266 user_interface.c wifi_station_set_default_hostname()

A critical NULL pointer dereference vulnerability in the ESP8266 firmware's `user_interface.c` allowed attackers to crash devices by exhausting the limited 80KB heap memory. The `wifi_station_set_default_hostname()` function's `os_malloc` call lacked a proper NULL guard, causing `ets_sprintf` to write to address 0 when allocation failed. The fix corrected a logic inversion in the NULL check condition.

high

Integer Overflow in PlayerAnimation.cpp memcpy Size Calculations

A critical integer overflow vulnerability was discovered in `animation/PlayerAnimation.cpp` where `vCount * sizeof(float) * 3` calculations could wrap around on 32-bit platforms when processing malicious animation files. An attacker could craft a model file with an oversized vertex count to trigger a heap buffer overflow via memcpy. The fix adds bounds checks against `SIZE_MAX` before all size computations used in memory copy operations.

high

Integer Overflow in wf_cliprdr.c: How Clipboard Data Could Corrupt Memory

A high-severity integer overflow vulnerability (CWE-190) was discovered in `libs/clipboard/src/windows/wf_cliprdr.c` at line 774, where the `m_nStreams` value derived from remote clipboard data was passed directly to `calloc()` without bounds validation. A malicious remote peer could supply a crafted stream count near `SIZE_MAX / sizeof(LPSTREAM)`, causing the size calculation to overflow and producing an undersized allocation that subsequent writes would overflow. The fix adds explicit bounds c