Skip to content

Incomplete __proto__ guard bypass allows prototype pollution via defaults argument #155

@BlackHatExploitation

Description

@BlackHatExploitation

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions