From 970e3f7bf0a02c41fa9d8e58191dafad02dbd94f Mon Sep 17 00:00:00 2001 From: Marshall Thompson Date: Wed, 3 Jun 2026 19:46:11 -0600 Subject: [PATCH] fix(feathers): skip prototype-polluting keys in _.merge Object.keys() returns __proto__ as an own enumerable key for JSON-parsed sources, causing the recursive merge to write onto Object.prototype. Skip __proto__/constructor/prototype keys. Reported-by: Andrew Ridings (@ridingsa) --- packages/feathers/src/commons.test.ts | 9 +++++++++ packages/feathers/src/commons.ts | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/packages/feathers/src/commons.test.ts b/packages/feathers/src/commons.test.ts index 424d82f08b..588a772cbd 100644 --- a/packages/feathers/src/commons.test.ts +++ b/packages/feathers/src/commons.test.ts @@ -213,5 +213,14 @@ describe('feathers/commons utils', () => { assert.equal(_.merge('hello', {}), 'hello') }) + + it('merge does not pollute Object.prototype', () => { + _.merge({}, JSON.parse('{"__proto__":{"polluted":"x"}}')) + _.merge({}, JSON.parse('{"constructor":{"prototype":{"polluted2":"y"}}}')) + assert.strictEqual(({} as any).polluted, undefined) + assert.strictEqual(({} as any).polluted2, undefined) + assert.strictEqual((Object.prototype as any).polluted, undefined) + assert.strictEqual((Object.prototype as any).polluted2, undefined) + }) }) }) diff --git a/packages/feathers/src/commons.ts b/packages/feathers/src/commons.ts index a6d06cbdaf..1a65d7e6c2 100644 --- a/packages/feathers/src/commons.ts +++ b/packages/feathers/src/commons.ts @@ -75,6 +75,10 @@ export const _ = { merge(target: any, source: any) { if (_.isObject(target) && _.isObject(source)) { Object.keys(source).forEach((key) => { + // Skip prototype-polluting keys (e.g. JSON-parsed `__proto__`) + if (key === '__proto__' || key === 'constructor' || key === 'prototype') { + return + } if (_.isObject(source[key])) { if (!target[key]) { Object.assign(target, { [key]: {} })