Defu, a deep-merge utility popular in the Nuxt.js and UnJS ecosystems, exposes applications to prototype pollution in versions up to 6.1.4. Developers pass unsanitized user input—think parsed JSON from requests, database records, or config files from untrusted sources—directly as the first argument to defu(). An attacker supplies a payload like {"__proto__":{"isAdmin":true}}, which pollutes the Object prototype and overrides server-side defaults.
This matters because defu handles configuration merging in high-stakes environments. Nuxt apps use it for runtimeConfig, modules, and user overrides. Polluting the prototype lets attackers inject properties that bypass explicit guards, turning a simple config merge into an authentication bypass or worse. For instance:
import { defu } from 'defu'
const userInput = JSON.parse('{"__proto__":{"isAdmin":true}}')
const config = defu(userInput, { isAdmin: false })
console.log(config.isAdmin) // true — attacker wins
Defu sees over 10 million weekly downloads on npm and powers thousands of projects, including major Nuxt deployments. A quick search shows 1,200+ dependents, from small tools to production sites. If your app parses untrusted data into defu without validation, you’re at risk. Prototype pollution has sunk apps before—remember the lodash merge vulns (CVE-2018-3721) or qs parser issues that led to DoS and RCE chains.
Root Cause
The bug hides in defu’s internal _defu function. It copies defaults using Object.assign({}, defaults). This invokes getters and setters on the target object, including __proto__. When attacker input sets __proto__ first, it swaps the prototype chain. Subsequent properties inherit from this polluted prototype, slipping past defu’s for...in loop that skips __proto__ keys explicitly.
JavaScript’s prototype system makes this tricky. Object.assign is fast but unsafe for untrusted data—it’s a known pollution vector since at least 2018. Defu’s guard only blocks direct __proto__ assignment, not prototype swaps. Fair critique: the library assumed sanitized input, but in 2024, no merge lib should.
Fix and Affected Versions
Update to defu 6.1.5 or later. The patch swaps Object.assign({}, defaults) for object spread: { ...defaults }. Spread uses [[DefineOwnProperty]] internally, skipping setters like __proto__. Clean, low-risk change—no performance hit worth noting for config merging.
Test it yourself:
// Vulnerable (<=6.1.4)
const config = defu(userInput, { isAdmin: false })
// Fixed (6.1.5+)
const config = defu(userInput, { isAdmin: false })
console.log(config.isAdmin) // false — safe
Immediate mitigations: Sanitize input before defu. Strip __proto__, constructor, and prototype keys recursively. Libraries like proto-strip or a custom replacer work. In Nuxt, validate runtimeConfig schemas with Zod or Yup. Avoid merging raw JSON—parse and filter first.
Why This Still Happens—and What It Means
Prototype pollution remains a top JS vuln (OWASP Top 10 adjacent via A03 Injection). We’ve seen it in Snyk, Retool, and countless npm pkgs. Defu fixed it fast—props to reporter @BlackHatExploitation—but exposure lingers until updates roll out. Scan your deps: npm ls defu or Snyk/Snyk Open Source.
Implications extend beyond auth bypass. Polluted prototypes taint global objects, enabling DoS via infinite loops or property exhaustion. In serverless Nuxt (Nitro), it could chain to RCE if configs control exec paths. Finance/crypto apps using defu for wallet configs? Double risk—override fee limits or keys.
Bottom line: Audit merges with user data. Prototype pollution isn’t “new,” but lax input handling keeps it deadly. Patch defu now, harden inputs always. In security, defaults win until proven safe.