Environment
defu: 6.1.4 (latest)
Node.js: v18.x / v20.x / v22.x (all affected)
Reproduction
// reproduce.mjs
import { defu } from 'defu'
const malicious = JSON.parse('{"proto":{"isAdmin":true}}')
const result = defu(malicious, { isAdmin: false })
console.log(result.isAdmin) // expected: false — actual: true
console.log(Object.prototype.hasOwnProperty.call(result, 'isAdmin')) // true (own property)
node reproduce.mjs
Describe the bug
Describe the bug
The proto guard introduced in v6.1.4 is incomplete.
The guard at src/defu.ts:18 only protects against proto appearing as a key of baseObject during the for...in loop. It does not protect the earlier Object.assign({}, defaults) call at line 15.
When defaults is a JSON.parse() result containing proto as an own enumerable property, Object.assign({}, defaults) invokes the proto setter on the new {} object, modifying its [[Prototype]]. In the next reduce iteration,
for...in walks that prototype chain, yielding the attacker-controlled property names. These names are not "proto" so they pass the guard and land as own properties in the final result — silently overriding any server-supplied
defaults.
This has security implications for applications that call defu(requestBody, serverDefaults). I am disclosing this privately first — please see the linked security advisory. This public issue is only to make the maintainers aware a
private report has been filed.
Fix: change line 15 from:
const object = Object.assign({}, defaults);
to:
const object = Object.assign(Object.create(null), defaults);
Full technical details, PoC, and impact analysis are in the private advisory.
Additional context
No response
Logs
Environment
defu: 6.1.4 (latest)
Node.js: v18.x / v20.x / v22.x (all affected)
Reproduction
// reproduce.mjs
import { defu } from 'defu'
const malicious = JSON.parse('{"proto":{"isAdmin":true}}')
const result = defu(malicious, { isAdmin: false })
console.log(result.isAdmin) // expected: false — actual: true
console.log(Object.prototype.hasOwnProperty.call(result, 'isAdmin')) // true (own property)
node reproduce.mjs
Describe the bug
Describe the bug
The proto guard introduced in v6.1.4 is incomplete.
The guard at src/defu.ts:18 only protects against proto appearing as a key of baseObject during the for...in loop. It does not protect the earlier Object.assign({}, defaults) call at line 15.
When defaults is a JSON.parse() result containing proto as an own enumerable property, Object.assign({}, defaults) invokes the proto setter on the new {} object, modifying its [[Prototype]]. In the next reduce iteration,
for...in walks that prototype chain, yielding the attacker-controlled property names. These names are not "proto" so they pass the guard and land as own properties in the final result — silently overriding any server-supplied
defaults.
This has security implications for applications that call defu(requestBody, serverDefaults). I am disclosing this privately first — please see the linked security advisory. This public issue is only to make the maintainers aware a
private report has been filed.
Fix: change line 15 from:
const object = Object.assign({}, defaults);
to:
const object = Object.assign(Object.create(null), defaults);
Full technical details, PoC, and impact analysis are in the private advisory.
Additional context
No response
Logs