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) {