A prototype pollution vulnerability in the defu library affects versions up to 6.1.4. Attackers can exploit it by injecting a __proto__ key into unsanitized user input passed as the first argument to defu(). This overrides default configurations, potentially granting unauthorized access or altering application behavior.
Defu, a lightweight deep-merge utility from the UnJS ecosystem, powers configuration handling in popular frameworks like Nuxt 3 and Nitro. It processes millions of weekly downloads on npm—over 10 million in recent months. Developers use it to merge user-provided settings with secure defaults, such as API configs or runtime options. If those user inputs come from untrusted sources like JSON request bodies or database records, pollution strikes.
Exploitation in Action
Consider a server endpoint that parses JSON from a client and merges it with defaults:
import { defu } from 'defu'
const userInput = JSON.parse('{"__proto__":{"isAdmin":true}}')
const config = defu(userInput, { isAdmin: false })
console.log(config.isAdmin) // true — attacker wins
Here, the attacker’s payload pollutes the Object prototype. Any subsequent object inherits isAdmin: true, bypassing the library’s intended safeguards. In a real app, this could flip authentication flags, expose admin panels, or inject malicious handlers into server configs.
Why does this matter? Prototype pollution has led to high-impact breaches before—think the 2018 lodash.merge vuln (CVE-2018-3721) that hit supply chains. In Node.js environments like Nitro serverless functions, a polluted prototype might taint global configs, enabling remote code execution via eval-like merges or prototype-based gadgets.
Root Cause and Technical Breakdown
The flaw hides in defu’s internal _defu function. It copies defaults using Object.assign({}, defaults). This invokes the __proto__ setter on the target object, swapping its prototype with attacker data. Later, a for...in loop skips __proto__ explicitly but misses inherited properties from the now-tainted prototype.
JavaScript’s prototype chain makes this tricky. Object.assign treats __proto__ specially per spec, unlike safer alternatives. The guard in defu—checking if (key === '__proto__') continue—fails because pollution happens upstream.
Defu maintainers acted fast after @BlackHatExploitation reported it. Version 6.1.5 swaps Object.assign for object spread: { ...defaults }. Spread uses internal [[DefineOwnProperty]] methods, skipping setters entirely. Clean fix, no regressions reported.
Skeptical note: Prototype pollution remains a persistent JavaScript plague. Libraries like deepmerge, merge, and even core utils have fallen to it repeatedly. Defu’s small footprint (under 1KB min+gzip) invites blind trust in the Nuxt ecosystem, where it’s a runtime dependency for thousands of apps.
Mitigation and Broader Implications
Upgrade to defu 6.1.5 or later immediately. Run npm ls defu to check versions across your deps—Nuxt users might pull it indirectly via @nuxt/kit or nitro. Audit code for direct defu(userInput, defaults) patterns with untrusted first args.
General hardening: Sanitize inputs before merging. Strip __proto__, constructor, and prototype keys recursively. Use libraries like safe-object-merge or validator.js for proto checks. In production, run with --disable-proto=delete Node flag to neuter __proto__ entirely.
This vuln underscores supply chain risks in JS frameworks. Nuxt 3 apps, deployed to Vercel or Cloudflare via Nitro, face amplified exposure—serverless scale means one flawed config propagates fast. With defu’s ubiquity (integrated in 50+ UnJS packages), unpatched instances linger. Scan your deps with npm audit or Snyk; false negatives on proto pollution are common.
Bottom line: Fix now, but treat every merge lib with suspicion. Prototype pollution exploits require no auth, just a JSON POST. In crypto or finance apps—where Njalla operates—this could leak keys or flip trade toggles. Stay vigilant; JS inheritance chains are a minefield.