From fab86c81966680ceb6be203d854823973065250f Mon Sep 17 00:00:00 2001 From: Diego Andai Date: Tue, 11 Jun 2024 08:54:24 -0400 Subject: [PATCH] [cherry-pick][utils] Fix GitHub-reported prototype pollution vulnerability in `deepmerge` (#41652) (#42608) --- .../mui-utils/src/deepmerge/deepmerge.test.ts | 56 +++++++++++++++++-- packages/mui-utils/src/deepmerge/deepmerge.ts | 12 ++-- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/packages/mui-utils/src/deepmerge/deepmerge.test.ts b/packages/mui-utils/src/deepmerge/deepmerge.test.ts index db39f72df32499..9cb09790761553 100644 --- a/packages/mui-utils/src/deepmerge/deepmerge.test.ts +++ b/packages/mui-utils/src/deepmerge/deepmerge.test.ts @@ -4,11 +4,59 @@ import deepmerge from './deepmerge'; describe('deepmerge', () => { // https://snyk.io/blog/after-three-years-of-silence-a-new-jquery-prototype-pollution-vulnerability-emerges-once-again/ - it('should not be subject to prototype pollution', () => { - deepmerge({}, JSON.parse('{ "myProperty": "a", "__proto__" : { "isAdmin" : true } }'), { - clone: false, - }); + it('should not be subject to prototype pollution via __proto__', () => { + const result = deepmerge( + {}, + JSON.parse('{ "myProperty": "a", "__proto__" : { "isAdmin" : true } }'), + { + clone: false, + }, + ); + + // @ts-expect-error __proto__ is not on this object type + // eslint-disable-next-line no-proto + expect(result.__proto__).to.have.property('isAdmin'); + expect({}).not.to.have.property('isAdmin'); + }); + + // https://cwe.mitre.org/data/definitions/915.html + it('should not be subject to prototype pollution via constructor', () => { + const result = deepmerge( + {}, + JSON.parse('{ "myProperty": "a", "constructor" : { "prototype": { "isAdmin" : true } } }'), + { + clone: true, + }, + ); + + expect(result.constructor.prototype).to.have.property('isAdmin'); + expect({}).not.to.have.property('isAdmin'); + }); + + // https://cwe.mitre.org/data/definitions/915.html + it('should not be subject to prototype pollution via prototype', () => { + const result = deepmerge( + {}, + JSON.parse('{ "myProperty": "a", "prototype": { "isAdmin" : true } }'), + { + clone: false, + }, + ); + + // @ts-expect-error prototype is not on this object type + expect(result.prototype).to.have.property('isAdmin'); + expect({}).not.to.have.property('isAdmin'); + }); + + it('should appropriately copy the fields without prototype pollution', () => { + const result = deepmerge( + {}, + JSON.parse('{ "myProperty": "a", "__proto__" : { "isAdmin" : true } }'), + ); + // @ts-expect-error __proto__ is not on this object type + // eslint-disable-next-line no-proto + expect(result.__proto__).to.have.property('isAdmin'); expect({}).not.to.have.property('isAdmin'); }); diff --git a/packages/mui-utils/src/deepmerge/deepmerge.ts b/packages/mui-utils/src/deepmerge/deepmerge.ts index a8035a0c470177..e04e90a7851d7e 100644 --- a/packages/mui-utils/src/deepmerge/deepmerge.ts +++ b/packages/mui-utils/src/deepmerge/deepmerge.ts @@ -41,12 +41,12 @@ export default function deepmerge( if (isPlainObject(target) && isPlainObject(source)) { Object.keys(source).forEach((key) => { - // Avoid prototype pollution - if (key === '__proto__') { - return; - } - - if (isPlainObject(source[key]) && key in target && isPlainObject(target[key])) { + if ( + isPlainObject(source[key]) && + // Avoid prototype pollution + Object.prototype.hasOwnProperty.call(target, key) && + isPlainObject(target[key]) + ) { // Since `output` is a clone of `target` and we have narrowed `target` in this block we can cast to the same type. (output as Record)[key] = deepmerge(target[key], source[key], options); } else if (options.clone) {