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/__tests__/unit/lodash/mix.spec.ts b/__tests__/unit/lodash/mix.spec.ts new file mode 100644 index 0000000..83a857a --- /dev/null +++ b/__tests__/unit/lodash/mix.spec.ts @@ -0,0 +1,25 @@ +import mix from '../../../src/lodash/mix'; + +describe('mix', () => { + it('merges plain objects', () => { + const result = mix({} as any, { a: 1 }, { b: 2 }); + expect(result).toEqual({ a: 1, b: 2 }); + }); + + it('does not pollute Object.prototype via __proto__', () => { + const payload = JSON.parse('{"__proto__": {"polluted": true}}'); + mix({}, payload); + expect((Object.prototype as any).polluted).toBeUndefined(); + }); + + it('does not pollute via constructor key', () => { + const payload = JSON.parse('{"constructor": {"prototype": {"polluted": true}}}'); + mix({}, payload); + expect((Object.prototype as any).polluted).toBeUndefined(); + }); + + it('does not pollute via prototype key', () => { + mix({} as any, { prototype: { polluted: true } } as any); + expect((Object.prototype as any).polluted).toBeUndefined(); + }); +}); diff --git a/package.json b/package.json index 2eb4758..6c05f88 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@antv/util", - "version": "3.3.11", + "version": "3.3.12", "license": "MIT", "sideEffects": false, "main": "lib/index.js", 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])) { diff --git a/src/lodash/mix.ts b/src/lodash/mix.ts index 2a730e9..2bde5ad 100644 --- a/src/lodash/mix.ts +++ b/src/lodash/mix.ts @@ -1,7 +1,15 @@ // FIXME: Mutable param should be forbidden in static lang. function _mix(dist: Base & Source, obj: Source): void { for (const key in obj) { - if (obj.hasOwnProperty(key) && key !== 'constructor' && obj[key] !== undefined) { + // Prevent prototype pollution by skipping dangerous keys + if ( + key === '__proto__' || + key === 'constructor' || + key === 'prototype' + ) { + continue; + } + if (obj.hasOwnProperty(key) && obj[key] !== undefined) { (dist)[key] = obj[key]; } }