From 42eb721eebc95cfded8d6c70cf62f3fbe83f3246 Mon Sep 17 00:00:00 2001 From: Jay Date: Sun, 19 Apr 2026 16:04:04 +0200 Subject: [PATCH] fix: replace in with has own prop util (#10761) --- lib/core/mergeConfig.js | 4 +-- tests/unit/prototypePollution.test.js | 38 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/lib/core/mergeConfig.js b/lib/core/mergeConfig.js index ce7afb6d..98b56e2d 100644 --- a/lib/core/mergeConfig.js +++ b/lib/core/mergeConfig.js @@ -56,9 +56,9 @@ export default function mergeConfig(config1, config2) { // eslint-disable-next-line consistent-return function mergeDirectKeys(a, b, prop) { - if (prop in config2) { + if (utils.hasOwnProp(config2, prop)) { return getMergedValue(a, b); - } else if (prop in config1) { + } else if (utils.hasOwnProp(config1, prop)) { return getMergedValue(undefined, a); } } diff --git a/tests/unit/prototypePollution.test.js b/tests/unit/prototypePollution.test.js index 63f13df2..cab0472f 100644 --- a/tests/unit/prototypePollution.test.js +++ b/tests/unit/prototypePollution.test.js @@ -20,6 +20,7 @@ describe('Prototype Pollution Protection', () => { delete Object.prototype.lookup; delete Object.prototype.family; delete Object.prototype.http2Options; + delete Object.prototype.validateStatus; }); describe('utils.merge', () => { @@ -275,6 +276,43 @@ describe('Prototype Pollution Protection', () => { }); }); + // GHSA-w9j2-pvgh-6h63: mergeDirectKeys must not inherit validateStatus from + // Object.prototype (was using the `in` operator which traverses the chain). + describe('GHSA-w9j2-pvgh-6h63 validateStatus merge', () => { + it('should not inherit a polluted validateStatus during mergeConfig', () => { + Object.prototype.validateStatus = () => true; + + const merged = mergeConfig(defaults, { url: '/x' }); + + assert.strictEqual(merged.validateStatus, defaults.validateStatus); + }); + + it('should keep 4xx/5xx responses rejected when Object.prototype.validateStatus is polluted', async () => { + Object.prototype.validateStatus = () => true; + + const server = http.createServer((req, res) => { + res.writeHead(401, { 'Content-Type': 'application/json' }); + res.end('{"error":"unauthorized"}'); + }); + + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + const { port } = server.address(); + + try { + let threw = false; + try { + await axios.get(`http://127.0.0.1:${port}/`); + } catch (err) { + threw = true; + assert.strictEqual(err.response.status, 401); + } + assert.strictEqual(threw, true); + } finally { + await new Promise((resolve) => server.close(resolve)); + } + }, 10000); + }); + // GHSA-3w6x-2g7m-8v23: end-to-end check that a polluted parseReviver does not // tamper with JSON response bodies through the full axios.get pipeline. describe('GHSA-3w6x-2g7m-8v23 parseReviver end-to-end', () => {