diff --git a/.gitignore b/.gitignore index b202105..b064b15 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ lerna-debug.log* # Node node_modules/ yarn.lock +package-lock.json # Build dist diff --git a/__tests__/unit/lodash/deep-mix.spec.ts b/__tests__/unit/lodash/deep-mix.spec.ts new file mode 100644 index 0000000..ebf3cff --- /dev/null +++ b/__tests__/unit/lodash/deep-mix.spec.ts @@ -0,0 +1,30 @@ +import deepMix from '../../../src/lodash/deep-mix'; + +describe('deepMix', () => { + it('merges plain objects', () => { + const result = deepMix({}, { a: 1 }, { b: 2 }); + expect(result).toEqual({ a: 1, b: 2 }); + }); + + it('deep merges nested objects', () => { + const result = deepMix({ a: { x: 1 } }, { a: { y: 2 } }); + expect(result).toEqual({ a: { x: 1, y: 2 } }); + }); + + it('does not pollute Object.prototype via __proto__', () => { + const payload = JSON.parse('{"__proto__": {"polluted": true}}'); + deepMix({}, payload); + expect((Object.prototype as any).polluted).toBeUndefined(); + }); + + it('does not pollute via constructor.prototype', () => { + const payload = JSON.parse('{"constructor": {"prototype": {"polluted": true}}}'); + deepMix({}, payload); + expect((Object.prototype as any).polluted).toBeUndefined(); + }); + + it('does not pollute via prototype key', () => { + deepMix({}, { prototype: { polluted: true } }); + expect((Object.prototype as any).polluted).toBeUndefined(); + }); +}); diff --git a/src/lodash/deep-mix.ts b/src/lodash/deep-mix.ts index a37df2c..0069941 100644 --- a/src/lodash/deep-mix.ts +++ b/src/lodash/deep-mix.ts @@ -18,6 +18,10 @@ function _deepMix(dist, src, level?, maxLevel?) { maxLevel = maxLevel || MAX_MIX_LEVEL; for (const key in src) { if (hasOwn(src, key)) { + // Prevent prototype pollution by skipping dangerous keys + if (key === '__proto__' || key === 'constructor' || key === 'prototype') { + continue; + } const value = src[key]; if (value !== null && isPlainObject(value)) { if (!isPlainObject(dist[key])) {