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
- CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
- OWASP Top 10 A08:2021 — Software and Data Integrity Failures (covers supply chain and dependency integrity)
- Node.js Security Best Practices: https://nodejs.org/en/docs/guides/security
- Snyk Learn — Prototype Pollution: https://learn.snyk.io/lessons/prototype-pollution/javascript/
Key Takeaways
defu6.1.4's key iteration skipped no dangerous keys — any caller passing{"__proto__": {...}}as the defaults argument could silently polluteObject.prototypefor the entire Node.js process.- Transitive dependencies are just as dangerous as direct ones —
defuwasn't inblog-site's direct dependencies, yet it was reachable and exploitable; theoverridesfield inpackage.jsonwas necessary to force the safe version across the whole tree. - The lock file integrity hash matters — the updated
integrityfield inpackage-lock.jsonfordefu6.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.