CVE-2021-25914: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') (CWE-1321)
value: Prototype pollution vulnerability in 'object-collider' versions 1.0.0 through 1.0.3 allows attacker to cause a denial of service and may lead to remote code execution.
CWE description: The product receives input from an upstream component that specifies attributes that are to be initialized or updated in an object, but it does not properly control modifications of attributes of the object prototype.
CVSS v3 base score: 9.8
Published: 2021-03-01T18:15Z
Repository: https://github.com/FireBlinkLTD/object-collider
Language: TypeScript
Files changed: src/utils/CollideUtil.ts, test/unit/CollideUtilTestSuite.ts
Commit date: 2021-02-24 18:07:19 +0200
Commit message: fix: __proto__ property merge

Diff: diff --git a/src/utils/CollideUtil.ts b/src/utils/CollideUtil.ts
index 581f273..27df8ae 100644
--- a/src/utils/CollideUtil.ts
+++ b/src/utils/CollideUtil.ts
@@ -23,11 +23,11 @@ export function collide(arg1: any, arg2: any, modifiers?: ICollideModifiers, sta
  * @param arg2 second argument to collide from
  * @param modifiers
  * @param startPath the entry path to start modifier path generation. Default value: $
  * @returns collide result
  */
-export function collideUnsafe(arg1: any, arg2: any,  modifiers?: ICollideModifiers, startPath: string = '$'): any {
+export function collideUnsafe(arg1: any, arg2: any, modifiers?: ICollideModifiers, startPath = '$'): any {
     if (arg2 === undefined) {
         return arg1;
     }
 
     if (isMissing(arg1)) {
@@ -76,18 +76,19 @@ function collideObjects(obj1: any, obj2: any, path: string, modifiers?: ICollide
         return modifiers[path](obj1, obj2);
     }
 
     for (const key of Object.keys(obj2)) {
         const subPath = path + '.' + key;
-
-        if (obj1[key] === undefined) {
-            obj1[key] = obj2[key];
-        } else {
-            if (modifiers && modifiers[subPath]) {
-                obj1[key] = modifiers[subPath](obj1[key], obj2[key]);
+        if (key !== '__proto__') {
+            if (obj1[key] === undefined) {
+                obj1[key] = obj2[key];
             } else {
-                obj1[key] = collideUnsafe(obj1[key], obj2[key], modifiers, subPath);
+                if (modifiers && modifiers[subPath]) {
+                    obj1[key] = modifiers[subPath](obj1[key], obj2[key]);
+                } else {
+                    obj1[key] = collideUnsafe(obj1[key], obj2[key], modifiers, subPath);
+                }
             }
         }
     }
 
     return obj1;
diff --git a/test/unit/CollideUtilTestSuite.ts b/test/unit/CollideUtilTestSuite.ts
index abd8485..04b297f 100644
--- a/test/unit/CollideUtilTestSuite.ts
+++ b/test/unit/CollideUtilTestSuite.ts
@@ -60,24 +60,21 @@ class CollideUtilTestSuite {
                 array: [11, 12],
             },
             override: 11,
         };
 
-        collideUnsafe(
-            obj1,
-            {
-                array: [3, 4],
-                obj: {
-                    b: false,
-                    d: 'd',
-                    e: 1,
-                    n: null,
-                    array: [12, 13, 14],
-                },
-                override: 111,
+        collideUnsafe(obj1, {
+            array: [3, 4],
+            obj: {
+                b: false,
+                d: 'd',
+                e: 1,
+                n: null,
+                array: [12, 13, 14],
             },
-        );
+            override: 111,
+        });
 
         assert.deepStrictEqual(obj1, {
             array: [1, 2, 3, 4],
             obj: {
                 a: true,
@@ -162,11 +159,11 @@ class CollideUtilTestSuite {
                     return b;
                 },
                 '#.custom.obj': (a: any, b: any) => Object.assign(a, b),
                 '#.custom.override': (a: any) => a,
             },
-            '#.custom'
+            '#.custom',
         );
 
         assert.deepStrictEqual(result, {
             array: [3, 4, 1, 2],
             obj: {
@@ -257,6 +254,30 @@ class CollideUtilTestSuite {
 
         chai.expect(() => {
             collide({ a: [1, 2] }, { a: true });
         }).to.throw('Unable to collide. Collide value at path $.a is not an array.');
     }
+
+    @test()
+    async testWithPrototypePollution(): Promise<void> {
+        const collideWith = JSON.parse(`{
+            "__proto__": {
+                "vulnerable": "yes"
+            },
+            "array": [3, 4]
+        }`);
+
+        console.log(collideWith);
+
+        const result = collide(
+            {
+                array: [1, 2],
+            },
+            collideWith,
+        );
+
+        assert.ok(!result.vulnerable);
+        assert.deepStrictEqual(result, {
+            array: [1, 2, 3, 4],
+        });
+    }
 }

Vulnerable code: export function collideUnsafe(arg1: any, arg2: any,  modifiers?: ICollideModifiers, startPath: string = '$'): any {

        if (obj1[key] === undefined) {
            obj1[key] = obj2[key];
        } else {
            if (modifiers && modifiers[subPath]) {
                obj1[key] = modifiers[subPath](obj1[key], obj2[key]);
                obj1[key] = collideUnsafe(obj1[key], obj2[key], modifiers, subPath);
        collideUnsafe(
            obj1,
            {
                array: [3, 4],
                obj: {
                    b: false,
                    d: 'd',
                    e: 1,
                    n: null,
                    array: [12, 13, 14],
                },
                override: 111,
        );
            '#.custom'
Fixed code: export function collideUnsafe(arg1: any, arg2: any, modifiers?: ICollideModifiers, startPath = '$'): any {
        if (key !== '__proto__') {
            if (obj1[key] === undefined) {
                obj1[key] = obj2[key];
                if (modifiers && modifiers[subPath]) {
                    obj1[key] = modifiers[subPath](obj1[key], obj2[key]);
                } else {
                    obj1[key] = collideUnsafe(obj1[key], obj2[key], modifiers, subPath);
                }
        collideUnsafe(obj1, {
            array: [3, 4],
            obj: {
                b: false,
                d: 'd',
                e: 1,
                n: null,
                array: [12, 13, 14],
            override: 111,
        });
            '#.custom',

    @test()
    async testWithPrototypePollution(): Promise<void> {
        const collideWith = JSON.parse(`{
            "__proto__": {
                "vulnerable": "yes"
            },
            "array": [3, 4]
        }`);

        console.log(collideWith);

        const result = collide(
            {
                array: [1, 2],
            },
            collideWith,
        );

        assert.ok(!result.vulnerable);
        assert.deepStrictEqual(result, {
            array: [1, 2, 3, 4],
        });
    }