Defu, a popular deep-merge utility in the Nuxt and UnJS ecosystems, carries a high-severity prototype pollution vulnerability in versions up to 6.1.4. Attackers can craft payloads with a __proto__ key to override your application’s default configurations when you pass unsanitized user input as the first argument to defu(). Update to 6.1.5 or later immediately if you’re using it.
This flaw exposes Node.js applications to prototype pollution, a classic attack vector that poisons JavaScript’s object prototypes. Polluted prototypes affect all objects inheriting from Object.prototype, potentially leading to logic bypasses, denial-of-service, or remote code execution downstream. In the defu example, an attacker flips an isAdmin default from false to true:
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 heavy use in server-side rendering frameworks like Nuxt 3, where it merges runtime configs, user overrides, and presets. If your app parses JSON from requests, databases, or files without validation and feeds it into defu, you’re at risk. A single polluted prototype cascades: that tainted isAdmin might grant unauthorized access across sessions or endpoints.
Root Cause
The bug hides in defu’s internal _defu function. It copies defaults using Object.assign({}, defaults), which triggers the __proto__ setter. This swaps the target’s prototype with attacker data. Later, a for...in loop skips __proto__ keys but misses inherited properties from the now-polluted prototype. Those slip into the merge result unchecked.
Object.assign’s behavior stems from JavaScript’s property descriptors. It calls getters/setters during assignment, unlike the safer object spread { ...defaults }, which relies on [[DefineOwnProperty]] and skips setters. This isn’t defu’s first rodeo with proto tricks—similar issues plague merge libraries like lodash (CVE-2018-3721) and haskell-merge. Node.js lacks built-in proto protection until recent ECMAScript proposals, so libraries must handle it manually.
Prototype pollution has bitten big: Think 2018’s lodash merge vuln affecting millions of npm packages, or 2022’s ForgeRock exploit chaining pollution to RCE. In defu’s case, severity hinges on input sources. Trusted configs? Low risk. Public APIs or file uploads? Critical.
Fix and Next Steps
The patch in 6.1.5 swaps Object.assign({}, defaults) for { ...defaults }. Simple, effective. No setter invocation, no proto swap. Verify by testing the exploit payload post-upgrade—it fails cleanly.
// Vulnerable (<=6.1.4)
Object.assign({}, defaults)
// Fixed (6.1.5+)
{ ...defaults }
Check your deps: npm ls defu or yarn’s why defu. Nuxt 3.8+ bundles fixed defu, but direct users or monorepos need manual updates. Run npm audit—it flags this as high (CVE pending). Broader mitigations:
- Sanitize inputs: Strip
__proto__,constructor,prototypekeys with a filter function before merging. - Use structured clones or JSON.parse/stringify to break prototypes.
- Audit merges: Tools like Snyk or Retire.js scan for known patterns.
- Runtime guards: Libraries like
proto-lockor Node’s--disable-proto=deleteflag (v20+).
Why this matters: Defu powers configs in production apps handling millions of requests daily. A polluted prototype isn’t theoretical—it alters runtime behavior globally. In finance or security apps, this flips auth flags or injects false positives. The reporter, @BlackHatExploitation, deserves credit for spotting it quickly; maintainers fixed it in hours. But it underscores a persistent Node.js pitfall: merge utils remain a top attack surface. Always validate user data, even in “safe” libraries. Scan now, merge carefully.
Defu’s npm downloads hit 10M+ weekly, mostly indirect via Nuxt. If you’re in that stack, this hits home. No evidence of active exploits yet, but CVEs like this draw copycats. Prioritize the update—it’s low-effort insurance against a high-impact chain.