CVE-2021-28860: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') (CWE-1321)
value: In Node.js mixme, prior to v0.5.1, an attacker can add or alter properties of an object via '__proto__' through the mutate() and merge() functions. The polluted attribute will be directly assigned to every object in the program. This will put the availability of the program at risk causing a potential denial of service (DoS).
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.1
Published: 2021-05-03T12:15Z
Repository: https://github.com/adaltas/node-mixme
Language: JavaScript
Files changed: CHANGELOG.md, dist/mixme.cjs.js, dist/mixme.esm.js, dist/mixme.umd.js, lib/index.js, src/index.coffee, test/merge.coffee, test/mutate.coffee
Commit date: 2021-04-25 23:39:08 +0200
Commit message: fix: dont pollute object proto #1

Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9d8803a..90bbffa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,13 @@
 
 # Changelog
 
+## Trunk
+
+* fix: dont pollute object proto #1
+* chore: latest dependencies
+
 ## Version 0.5.0
 
 * feat: support object with null prototype
 
 ## Version 0.4.0
diff --git a/dist/mixme.cjs.js b/dist/mixme.cjs.js
index 84da66c..c071ccf 100644
--- a/dist/mixme.cjs.js
+++ b/dist/mixme.cjs.js
@@ -17,11 +17,11 @@ function _typeof(obj) {
 
   return _typeof(obj);
 }
 
 // Generated by CoffeeScript 2.5.1
-var _snake_case;
+var _snake_case; exports.clone = void 0; exports.compare = void 0; exports.is_object = void 0; exports.is_object_literal = void 0; exports.merge = void 0; exports.mutate = void 0; exports.snake_case = void 0;
 
 exports.merge = function merge() {
   return exports.mutate.apply(void 0, [{}].concat(Array.prototype.slice.call(arguments)));
 };
 
@@ -48,10 +48,14 @@ exports.mutate = function mutate() {
       if (!exports.is_object_literal(target)) {
         target = {};
       }
 
       for (name in source) {
+        if (name === '__proto__') {
+          continue;
+        }
+
         target[name] = exports.mutate(target[name], source[name]);
       }
     } else if (Array.isArray(source)) {
       target = function () {
         var k, len, results;
diff --git a/dist/mixme.esm.js b/dist/mixme.esm.js
index 87f2cfc..e4adb55 100644
--- a/dist/mixme.esm.js
+++ b/dist/mixme.esm.js
@@ -44,10 +44,14 @@ _mutate = function mutate() {
       if (!is_object_literal(target)) {
         target = {};
       }
 
       for (name in source) {
+        if (name === '__proto__') {
+          continue;
+        }
+
         target[name] = _mutate(target[name], source[name]);
       }
     } else if (Array.isArray(source)) {
       target = function () {
         var k, len, results;
diff --git a/dist/mixme.umd.js b/dist/mixme.umd.js
index 6dc309c..28adeb2 100644
--- a/dist/mixme.umd.js
+++ b/dist/mixme.umd.js
@@ -19,11 +19,11 @@
 
     return _typeof(obj);
   }
 
   // Generated by CoffeeScript 2.5.1
-  var _snake_case;
+  var _snake_case; exports.clone = void 0; exports.compare = void 0; exports.is_object = void 0; exports.is_object_literal = void 0; exports.merge = void 0; exports.mutate = void 0; exports.snake_case = void 0;
 
   exports.merge = function merge() {
     return exports.mutate.apply(void 0, [{}].concat(Array.prototype.slice.call(arguments)));
   };
 
@@ -50,10 +50,14 @@
         if (!exports.is_object_literal(target)) {
           target = {};
         }
 
         for (name in source) {
+          if (name === '__proto__') {
+            continue;
+          }
+
           target[name] = exports.mutate(target[name], source[name]);
         }
       } else if (Array.isArray(source)) {
         target = function () {
           var k, len, results;
diff --git a/lib/index.js b/lib/index.js
index 22e4f4c..261b402 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -25,10 +25,13 @@ mutate = function() {
     if (is_object_literal(source)) {
       if (!is_object_literal(target)) {
         target = {};
       }
       for (name in source) {
+        if (name === '__proto__') {
+          continue;
+        }
         target[name] = mutate(target[name], source[name]);
       }
     } else if (Array.isArray(source)) {
       target = (function() {
         var k, len, results;
diff --git a/src/index.coffee b/src/index.coffee
index e7b2318..155446a 100644
--- a/src/index.coffee
+++ b/src/index.coffee
@@ -17,10 +17,11 @@ mutate = ->
   for i in [1 ... arguments.length]
     source = arguments[i]
     if is_object_literal source
       target = {} unless is_object_literal target
       for name of source
+        continue if name is '__proto__'
         target[name] = mutate target[name], source[name]
     else if Array.isArray source
       target = for v in source
         mutate undefined, v
     else unless source is undefined
diff --git a/test/merge.coffee b/test/merge.coffee
index 20f9df9..d895a32 100644
--- a/test/merge.coffee
+++ b/test/merge.coffee
@@ -23,5 +23,10 @@ describe 'mixme.merge', ->
     .should.eql a: 1, b: 2, c: 3, d: 4
     {...obj1}
     .should.eql a: 1, b: 2, c: 0
     obj2
     .should.eql a: 1, c: 3, d: 4
+
+  it 'dont merge proto', ->
+    merge {}, JSON.parse '{"__proto__": {"polluted": "ohno"}}'
+    obj = Object.create {}
+    should(obj.polluted).be.Undefined()
diff --git a/test/mutate.coffee b/test/mutate.coffee
index 2d7cecf..4fdde45 100644
--- a/test/mutate.coffee
+++ b/test/mutate.coffee
@@ -21,10 +21,16 @@ describe 'mutate', ->
       mutate obj1, obj2
       should(Object.getPrototypeOf obj1).be.null()
       {...obj1}
       .should.eql { a: 'a value', b: 'b new', c: { d: 'd new', f: 'f value'}}
 
+    it 'dont merge proto', ->
+      src = {}
+      mutate src, JSON.parse '{"__proto__": {"polluted": "ohno"}}'
+      obj = Object.create {}
+      should(obj.polluted).be.Undefined()
+
   describe '2nd arg not object', ->
     
     it 'object with string', ->
       obj1 = { a_key: 'a value', b_key: 'b value'}
       obj2 = 'b'

Vulnerable code: var _snake_case;
  var _snake_case;
Fixed code: ## Trunk

* fix: dont pollute object proto #1
* chore: latest dependencies

var _snake_case; exports.clone = void 0; exports.compare = void 0; exports.is_object = void 0; exports.is_object_literal = void 0; exports.merge = void 0; exports.mutate = void 0; exports.snake_case = void 0;
        if (name === '__proto__') {
          continue;
        }

        if (name === '__proto__') {
          continue;
        }

  var _snake_case; exports.clone = void 0; exports.compare = void 0; exports.is_object = void 0; exports.is_object_literal = void 0; exports.merge = void 0; exports.mutate = void 0; exports.snake_case = void 0;
          if (name === '__proto__') {
            continue;
          }

        if (name === '__proto__') {
          continue;
        }
        continue if name is '__proto__'

  it 'dont merge proto', ->
    merge {}, JSON.parse '{"__proto__": {"polluted": "ohno"}}'
    obj = Object.create {}
    should(obj.polluted).be.Undefined()
    it 'dont merge proto', ->
      src = {}
      mutate src, JSON.parse '{"__proto__": {"polluted": "ohno"}}'
      obj = Object.create {}
      should(obj.polluted).be.Undefined()