Skip to content

Commit 0d7ec33

Browse files
fix: __proto__ pollution (#4885)
1 parent 07a3906 commit 0d7ec33

File tree

2 files changed

+45
-6
lines changed

2 files changed

+45
-6
lines changed

lib/dispatcher/balanced-pool.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const {
1414
} = require('./pool-base')
1515
const Pool = require('./pool')
1616
const { kUrl } = require('../core/symbols')
17-
const { parseOrigin } = require('../core/util')
17+
const util = require('../core/util')
1818
const kFactory = Symbol('factory')
1919

2020
const kOptions = Symbol('options')
@@ -56,7 +56,10 @@ class BalancedPool extends PoolBase {
5656

5757
super()
5858

59-
this[kOptions] = opts
59+
this[kOptions] = { ...util.deepClone(opts) }
60+
this[kOptions].interceptors = opts.interceptors
61+
? { ...opts.interceptors }
62+
: undefined
6063
this[kIndex] = -1
6164
this[kCurrentWeight] = 0
6265

@@ -76,7 +79,7 @@ class BalancedPool extends PoolBase {
7679
}
7780

7881
addUpstream (upstream) {
79-
const upstreamOrigin = parseOrigin(upstream).origin
82+
const upstreamOrigin = util.parseOrigin(upstream).origin
8083

8184
if (this[kClients].find((pool) => (
8285
pool[kUrl].origin === upstreamOrigin &&
@@ -85,7 +88,7 @@ class BalancedPool extends PoolBase {
8588
))) {
8689
return this
8790
}
88-
const pool = this[kFactory](upstreamOrigin, Object.assign({}, this[kOptions]))
91+
const pool = this[kFactory](upstreamOrigin, this[kOptions])
8992

9093
this[kAddClient](pool)
9194
pool.on('connect', () => {
@@ -125,7 +128,7 @@ class BalancedPool extends PoolBase {
125128
}
126129

127130
removeUpstream (upstream) {
128-
const upstreamOrigin = parseOrigin(upstream).origin
131+
const upstreamOrigin = util.parseOrigin(upstream).origin
129132

130133
const pool = this[kClients].find((pool) => (
131134
pool[kUrl].origin === upstreamOrigin &&
@@ -141,7 +144,7 @@ class BalancedPool extends PoolBase {
141144
}
142145

143146
getUpstream (upstream) {
144-
const upstreamOrigin = parseOrigin(upstream).origin
147+
const upstreamOrigin = util.parseOrigin(upstream).origin
145148

146149
return this[kClients].find((pool) => (
147150
pool[kUrl].origin === upstreamOrigin &&

test/node-test/balanced-pool.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,3 +601,39 @@ describe('weighted round robin', () => {
601601
})
602602
}
603603
})
604+
605+
test('should not be vulnerable to __proto__ pollution via options', async (t) => {
606+
const { EventEmitter } = require('node:events')
607+
608+
let capturedOpts
609+
610+
// Simulate attacker-controlled options with __proto__ property
611+
const attackerOptions = JSON.parse(`
612+
{
613+
"__proto__": {
614+
"polluted": "YES",
615+
"connections": 1
616+
}
617+
}
618+
`)
619+
620+
attackerOptions.factory = (origin, opts) => {
621+
capturedOpts = opts
622+
623+
const stub = new EventEmitter()
624+
stub.dispatch = () => true
625+
stub.close = () => Promise.resolve()
626+
stub.destroy = () => Promise.resolve()
627+
stub.destroyed = false
628+
stub.closed = false
629+
630+
return stub
631+
}
632+
633+
new BalancedPool(['http://localhost/'], attackerOptions) // eslint-disable-line no-new
634+
635+
// Verify that the captured options do not have polluted prototype
636+
assert.strictEqual(capturedOpts.polluted, undefined, 'polluted property should not exist on options')
637+
assert.strictEqual(Object.getPrototypeOf(capturedOpts).polluted, undefined, 'prototype should not be polluted')
638+
assert.strictEqual({}.polluted, undefined, 'global Object.prototype should not be polluted')
639+
})

0 commit comments

Comments
 (0)