CVE-2020-7639: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') (CWE-1321)
value: eivindfjeldstad-dot below 1.0.3 is vulnerable to Prototype Pollution.The function 'set' could be tricked into adding or modifying properties of 'Object.prototype' using a '__proto__' payload.
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: 5.3
Published: 2020-04-06T13:15Z
Repository: https://github.com/eivindfjeldstad/dot
Language: JavaScript
Files changed: index.js, package.json, test/index.js
Commit date: 2020-02-22 19:36:16 +0100
Commit message: Prevent prototype pollution

Diff: diff --git a/index.js b/index.js
index 639b1bd..301996c 100644
--- a/index.js
+++ b/index.js
@@ -6,22 +6,25 @@
  * @param {Mixed} val
  * @return {Object}
  * @api public
  */
 
-exports.set = function (obj, path, val) {
+exports.set = function(obj, path, val) {
   var segs = path.split('.');
   var attr = segs.pop();
   var src = obj;
 
   for (var i = 0; i < segs.length; i++) {
     var seg = segs[i];
+    if (!isSafe(obj, seg)) return src;
     obj[seg] = obj[seg] || {};
     obj = obj[seg];
   }
 
-  obj[attr] = val;
+  if (isSafe(obj, attr)) {
+    obj[attr] = val;
+  }
 
   return src;
 };
 
 /**
@@ -31,11 +34,11 @@ exports.set = function (obj, path, val) {
  * @param {String} path
  * @return {Mixed}
  * @api public
  */
 
-exports.get = function (obj, path) {
+exports.get = function(obj, path) {
   var segs = path.split('.');
   var attr = segs.pop();
 
   for (var i = 0; i < segs.length; i++) {
     var seg = segs[i];
@@ -53,21 +56,44 @@ exports.get = function (obj, path) {
  * @param {String} path
  * @return {Mixed}
  * @api public
  */
 
-exports.delete = function (obj, path) {
+exports.delete = function(obj, path) {
   var segs = path.split('.');
   var attr = segs.pop();
 
   for (var i = 0; i < segs.length; i++) {
     var seg = segs[i];
     if (!obj[seg]) return;
+    if (!isSafe(obj, seg)) return;
     obj = obj[seg];
   }
 
+  if (!isSafe(obj, attr)) return;
+
   if (Array.isArray(obj)) {
-    obj.splice(path, 1);
+    obj.splice(attr, 1);
   } else {
     delete obj[attr];
   }
 };
+
+function isSafe(obj, prop) {
+  if (isObject(obj)) {
+    return obj[prop] === undefined || hasOwnProperty(obj, prop);
+  }
+
+  if (Array.isArray(obj)) {
+    return !isNaN(parseInt(prop, 10));
+  }
+
+  return false;
+}
+
+function hasOwnProperty(obj, prop) {
+  return Object.prototype.hasOwnProperty.call(obj, prop);
+}
+
+function isObject(obj) {
+  return Object.prototype.toString.call(obj) === '[object Object]';
+}
diff --git a/package.json b/package.json
index 5821ec0..2ea87e2 100644
--- a/package.json
+++ b/package.json
@@ -1,11 +1,11 @@
 {
   "name": "@eivifj/dot",
   "publishConfig": {
     "access": "public"
   },
-  "version": "1.0.2",
+  "version": "1.0.3",
   "description": "Get and set object properties with dot notation",
   "main": "index.js",
   "scripts": {
     "test": "node test"
   },
diff --git a/test/index.js b/test/index.js
index 1f5e4f5..f11b7ba 100644
--- a/test/index.js
+++ b/test/index.js
@@ -1,35 +1,62 @@
 var assert = require('assert');
 var dot = require('..');
 
-var tests = module.exports = {
-  'test set': function () {
+var tests = (module.exports = {
+  'test set': function() {
     var obj = {};
     var ret = dot.set(obj, 'cool.aid', 'rocks');
     assert(obj.cool.aid === 'rocks');
     assert(obj === ret);
   },
 
-  'test get': function () {
+  'test get': function() {
     var obj = {};
     obj.cool = {};
     obj.cool.aid = 'rocks';
     var value = dot.get(obj, 'cool.aid');
     assert(value === 'rocks');
   },
 
-  'test delete': function () {
+  'test delete': function() {
     var obj = {};
     obj.cool = {};
     obj.cool.aid = 'rocks';
     obj.cool.hello = ['world'];
     dot.delete(obj, 'cool.aid');
     dot.delete(obj, 'cool.hello.0');
-    assert(!obj.cool.hasOwnProperty('aid'))
+    assert(!obj.cool.hasOwnProperty('aid'));
     assert(obj.cool.hello.length == 0);
+  },
+
+  'test prototype pollution': function() {
+    var obj = {};
+    obj.cool = {};
+    obj.cool.aid = 'rocks';
+    obj.cool.hello = ['world'];
+    dot.set(obj, '__proto__', 'test');
+    dot.set(obj, '__proto__.toString', 'test');
+    dot.set(obj, 'toString', 'test');
+    dot.set(obj, 'cool.hello.__proto__', 'test');
+    dot.set(obj, 'cool.hello.__proto__.toString', 'test');
+    dot.set(obj, 'cool.hello.toString', 'test');
+    assert(obj.__proto__ === {}.__proto__);
+    assert(obj.toString === Object.prototype.toString);
+    assert(obj.cool.hello.__proto__ === [].__proto__);
+    assert(obj.cool.hello.toString === Array.prototype.toString);
+    dot.delete(obj, '__proto__.toString', 'test');
+    dot.delete(obj, '__proto__', 'test');
+    dot.delete(obj, 'toString', 'test');
+    dot.delete(obj, 'cool.hello.__proto__.toString', 'test');
+    dot.delete(obj, 'cool.hello.__proto__', 'test');
+    dot.delete(obj, 'cool.hello.toString', 'test');
+    assert(obj.__proto__ === {}.__proto__);
+    assert(obj.toString === Object.prototype.toString);
+    assert(obj.cool.hello.__proto__ === [].__proto__);
+    assert(obj.cool.hello.toString === Array.prototype.toString);
   }
-}
+});
 
 for (var t in tests) {
   tests[t]();
 }
 

Vulnerable code: exports.set = function (obj, path, val) {
  obj[attr] = val;
exports.get = function (obj, path) {
exports.delete = function (obj, path) {
    obj.splice(path, 1);
  "version": "1.0.2",
var tests = module.exports = {
  'test set': function () {
  'test get': function () {
  'test delete': function () {
    assert(!obj.cool.hasOwnProperty('aid'))
}
Fixed code: exports.set = function(obj, path, val) {
    if (!isSafe(obj, seg)) return src;
  if (isSafe(obj, attr)) {
    obj[attr] = val;
  }
exports.get = function(obj, path) {
exports.delete = function(obj, path) {
    if (!isSafe(obj, seg)) return;
  if (!isSafe(obj, attr)) return;

    obj.splice(attr, 1);

function isSafe(obj, prop) {
  if (isObject(obj)) {
    return obj[prop] === undefined || hasOwnProperty(obj, prop);
  }

  if (Array.isArray(obj)) {
    return !isNaN(parseInt(prop, 10));
  }

  return false;
}

function hasOwnProperty(obj, prop) {
  return Object.prototype.hasOwnProperty.call(obj, prop);
}

function isObject(obj) {
  return Object.prototype.toString.call(obj) === '[object Object]';
}
  "version": "1.0.3",
var tests = (module.exports = {
  'test set': function() {
  'test get': function() {
  'test delete': function() {
    assert(!obj.cool.hasOwnProperty('aid'));
  },

  'test prototype pollution': function() {
    var obj = {};
    obj.cool = {};
    obj.cool.aid = 'rocks';
    obj.cool.hello = ['world'];
    dot.set(obj, '__proto__', 'test');
    dot.set(obj, '__proto__.toString', 'test');
    dot.set(obj, 'toString', 'test');
    dot.set(obj, 'cool.hello.__proto__', 'test');
    dot.set(obj, 'cool.hello.__proto__.toString', 'test');
    dot.set(obj, 'cool.hello.toString', 'test');
    assert(obj.__proto__ === {}.__proto__);
    assert(obj.toString === Object.prototype.toString);
    assert(obj.cool.hello.__proto__ === [].__proto__);
    assert(obj.cool.hello.toString === Array.prototype.toString);
    dot.delete(obj, '__proto__.toString', 'test');
    dot.delete(obj, '__proto__', 'test');
    dot.delete(obj, 'toString', 'test');
    dot.delete(obj, 'cool.hello.__proto__.toString', 'test');
    dot.delete(obj, 'cool.hello.__proto__', 'test');
    dot.delete(obj, 'cool.hello.toString', 'test');
    assert(obj.__proto__ === {}.__proto__);
    assert(obj.toString === Object.prototype.toString);
    assert(obj.cool.hello.__proto__ === [].__proto__);
    assert(obj.cool.hello.toString === Array.prototype.toString);
});