From facdfe62ac544633f5a5ffe7cf82bd1efb3e7538 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 14 Oct 2026 17:18:39 -0400 Subject: [PATCH 1/4] fix(utils): prevent prototype pollution in deepExtend --- src/utils/deep-copy.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/utils/deep-copy.ts b/src/utils/deep-copy.ts index e4c2bddf85..ee22f8fb21 100644 --- a/src/utils/deep-copy.ts +++ b/src/utils/deep-copy.ts @@ -73,6 +73,9 @@ export function deepExtend(target: any, source: any): any { if (!Object.prototype.hasOwnProperty.call(source, prop)) { continue; } + if (prop === '__proto__' || prop === 'constructor' || prop === 'prototype') { + continue; + } target[prop] = deepExtend(target[prop], source[prop]); } From 3b3ae755d8eeaab7d2ee57c8fbab8d225b6ceb03 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 14 Oct 2026 18:32:06 -0400 Subject: [PATCH 2/4] test(utils): add unit tests for deepCopy and deepExtend prototype pollution --- test/unit/index.spec.ts | 1 + test/unit/utils/deep-copy.spec.ts | 84 +++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 test/unit/utils/deep-copy.spec.ts diff --git a/test/unit/index.spec.ts b/test/unit/index.spec.ts index 8e236b5231..495046421f 100644 --- a/test/unit/index.spec.ts +++ b/test/unit/index.spec.ts @@ -29,6 +29,7 @@ import './utils/validator.spec'; import './utils/api-request.spec'; import './utils/jwt.spec'; import './utils/crypto-signer.spec'; +import './utils/deep-copy.spec'; // Auth import './auth/auth.spec'; diff --git a/test/unit/utils/deep-copy.spec.ts b/test/unit/utils/deep-copy.spec.ts new file mode 100644 index 0000000000..b6db681d20 --- /dev/null +++ b/test/unit/utils/deep-copy.spec.ts @@ -0,0 +1,84 @@ +/*! + * @license + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { deepCopy, deepExtend } from '../../../src/utils/deep-copy'; + +describe('deepCopy()', () => { + it('should deep copy primitives and objects', () => { + const source = { + a: 1, + b: 'hello', + c: [1, 2, { nested: true }], + d: new Date(1234567890), + }; + const copy = deepCopy(source); + expect(copy).to.deep.equal(source); + expect(copy).to.not.equal(source); + expect(copy.c).to.not.equal(source.c); + expect(copy.c[2]).to.not.equal(source.c[2]); + expect(copy.d).to.not.equal(source.d); + }); +}); + +describe('deepExtend()', () => { + it('should merge nested objects and overwrite arrays', () => { + const target = { + a: { x: 1, y: 2 }, + arr: [1, 2], + }; + const source = { + a: { y: 3, z: 4 }, + arr: [3], + }; + const result = deepExtend(target, source); + expect(result).to.equal(target); + expect(result).to.deep.equal({ + a: { x: 1, y: 3, z: 4 }, + arr: [3], + }); + }); + + describe('prototype pollution', () => { + afterEach(() => { + delete (Object.prototype as any).polluted; + }); + + it('should not pollute prototype via __proto__', () => { + const payload = JSON.parse('{"__proto__": {"polluted": true}}'); + const target: Record = {}; + deepExtend(target, payload); + expect((Object.prototype as any).polluted).to.be.undefined; + expect(target.polluted).to.be.undefined; + }); + + it('should not pollute prototype via constructor', () => { + const payload = JSON.parse('{"constructor": {"prototype": {"polluted": true}}}'); + const target: Record = {}; + deepExtend(target, payload); + expect((Object.prototype as any).polluted).to.be.undefined; + expect(target.polluted).to.be.undefined; + }); + + it('should not copy prototype property onto target', () => { + const payload = JSON.parse('{"prototype": {"polluted": true}}'); + const target: Record = {}; + deepExtend(target, payload); + expect(Object.prototype.hasOwnProperty.call(target, 'prototype')).to.be.false; + }); + }); +}); From 14d2da523284c8532690cf0bb42c2cebbf900387 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 15 Oct 2026 10:37:01 -0400 Subject: [PATCH 3/4] fix(utils): avoid mutating inherited target properties in deepExtend --- src/utils/deep-copy.ts | 3 ++- test/unit/utils/deep-copy.spec.ts | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/utils/deep-copy.ts b/src/utils/deep-copy.ts index ee22f8fb21..03d9641a0d 100644 --- a/src/utils/deep-copy.ts +++ b/src/utils/deep-copy.ts @@ -76,7 +76,8 @@ export function deepExtend(target: any, source: any): any { if (prop === '__proto__' || prop === 'constructor' || prop === 'prototype') { continue; } - target[prop] = deepExtend(target[prop], source[prop]); + const hasProp = target instanceof Object && Object.prototype.hasOwnProperty.call(target, prop); + target[prop] = deepExtend(hasProp ? target[prop] : undefined, source[prop]); } return target; diff --git a/test/unit/utils/deep-copy.spec.ts b/test/unit/utils/deep-copy.spec.ts index b6db681d20..f1f0bfacfa 100644 --- a/test/unit/utils/deep-copy.spec.ts +++ b/test/unit/utils/deep-copy.spec.ts @@ -53,9 +53,22 @@ describe('deepExtend()', () => { }); }); + it('should not mutate inherited prototype properties of the target', () => { + const proto = { shared: { x: 1 } }; + const target = Object.create(proto); + const source = { shared: { y: 2 } }; + + deepExtend(target, source); + + expect(proto.shared).to.deep.equal({ x: 1 }); + expect(target.shared).to.deep.equal({ y: 2 }); + expect(Object.prototype.hasOwnProperty.call(target, 'shared')).to.be.true; + }); + describe('prototype pollution', () => { afterEach(() => { delete (Object.prototype as any).polluted; + delete (Object.prototype.toString as any).polluted; }); it('should not pollute prototype via __proto__', () => { @@ -80,5 +93,13 @@ describe('deepExtend()', () => { deepExtend(target, payload); expect(Object.prototype.hasOwnProperty.call(target, 'prototype')).to.be.false; }); + + it('should not pollute inherited Object.prototype methods like toString', () => { + const payload = JSON.parse('{"toString": {"polluted": true}}'); + const target: Record = {}; + deepExtend(target, payload); + expect((Object.prototype.toString as any).polluted).to.be.undefined; + expect(target.toString).to.deep.equal({ polluted: true }); + }); }); }); From 4d121c56f197ba366e7007e4c25d9dc798742843 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 15 Oct 2026 10:53:45 -0400 Subject: [PATCH 4/4] fix(utils): support null-prototype target objects in deepExtend --- src/utils/deep-copy.ts | 4 +++- test/unit/utils/deep-copy.spec.ts | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/utils/deep-copy.ts b/src/utils/deep-copy.ts index 03d9641a0d..20abd5bc06 100644 --- a/src/utils/deep-copy.ts +++ b/src/utils/deep-copy.ts @@ -76,7 +76,9 @@ export function deepExtend(target: any, source: any): any { if (prop === '__proto__' || prop === 'constructor' || prop === 'prototype') { continue; } - const hasProp = target instanceof Object && Object.prototype.hasOwnProperty.call(target, prop); + const hasProp = Boolean(target) && + (typeof target === 'object' || typeof target === 'function') && + Object.prototype.hasOwnProperty.call(target, prop); target[prop] = deepExtend(hasProp ? target[prop] : undefined, source[prop]); } diff --git a/test/unit/utils/deep-copy.spec.ts b/test/unit/utils/deep-copy.spec.ts index f1f0bfacfa..e72bcab372 100644 --- a/test/unit/utils/deep-copy.spec.ts +++ b/test/unit/utils/deep-copy.spec.ts @@ -65,6 +65,16 @@ describe('deepExtend()', () => { expect(Object.prototype.hasOwnProperty.call(target, 'shared')).to.be.true; }); + it('should merge properties when target is Object.create(null)', () => { + const target = Object.create(null); + target.a = { b: 1 }; + const source = { a: { c: 2 } }; + + deepExtend(target, source); + + expect(target.a).to.deep.equal({ b: 1, c: 2 }); + }); + describe('prototype pollution', () => { afterEach(() => { delete (Object.prototype as any).polluted;