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];
}
}