CVE-2020-7707: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') (CWE-1321)
value: The package property-expr before 2.0.3 are vulnerable to Prototype Pollution via the setter function.
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: 2020-08-18T14:15Z
Repository: https://github.com/jquense/expr
Language: JavaScript
Files changed: compiler.js, index.js, test.js
Commit date: 2020-08-17 13:33:41 -0400
Commit message: fix: prototype polution vector

Diff: diff --git a/compiler.js b/compiler.js
index 1eed39a..70339b8 100644
--- a/compiler.js
+++ b/compiler.js
@@ -6,11 +6,11 @@ var setCache = new Cache(512),
 function makeSafe(path, param) {
   var result = param,
     parts = split(path),
     isLast
 
-  forEach(parts, function(part, isBracket, isArray, idx, parts) {
+  forEach(parts, function (part, isBracket, isArray, idx, parts) {
     isLast = idx === parts.length - 1
 
     part = isBracket || isArray ? '[' + part + ']' : '.' + part
 
     result += part + (!isLast ? ' || {})' : ')')
@@ -34,26 +34,34 @@ function expr(expression, safe, param) {
   return safe ? makeSafe(expression, param) : param + expression
 }
 
 module.exports = {
   expr,
-  setter: function(path) {
+  setter: function (path) {
+    if (
+      path.indexOf('__proto__') !== -1 ||
+      path.indexOf('constructor') !== -1 ||
+      path.indexOf('prototype') !== -1
+    ) {
+      return (obj) => obj
+    }
+
     return (
       setCache.get(path) ||
       setCache.set(
         path,
         new Function('data, value', expr(path, 'data') + ' = value')
       )
     )
   },
 
-  getter: function(path, safe) {
+  getter: function (path, safe) {
     var key = path + '_' + safe
     return (
       getCache.get(key) ||
       getCache.set(
         key,
         new Function('data', 'return ' + expr(path, safe, 'data'))
       )
     )
-  }
+  },
 }
diff --git a/index.js b/index.js
index e2f305d..d323ae9 100644
--- a/index.js
+++ b/index.js
@@ -5,18 +5,18 @@
 
 function Cache(maxSize) {
   this._maxSize = maxSize
   this.clear()
 }
-Cache.prototype.clear = function() {
+Cache.prototype.clear = function () {
   this._size = 0
   this._values = Object.create(null)
 }
-Cache.prototype.get = function(key) {
+Cache.prototype.get = function (key) {
   return this._values[key]
 }
-Cache.prototype.set = function(key, value) {
+Cache.prototype.set = function (key, value) {
   this._size >= this._maxSize && this.clear()
   if (!(key in this._values)) this._size++
 
   return (this._values[key] = value)
 }
@@ -39,27 +39,38 @@ module.exports = {
 
   split: split,
 
   normalizePath: normalizePath,
 
-  setter: function(path) {
+  setter: function (path) {
     var parts = normalizePath(path)
 
     return (
       setCache.get(path) ||
-      setCache.set(path, function setter(data, value) {
-        var index = 0,
-          len = parts.length
+      setCache.set(path, function setter(obj, value) {
+        var index = 0
+        var len = parts.length
+        var data = obj
+
         while (index < len - 1) {
+          let part = parts[index]
+          if (
+            part === '__proto__' ||
+            part === 'constructor' ||
+            part === 'prototype'
+          ) {
+            return obj
+          }
+
           data = data[parts[index++]]
         }
         data[parts[index]] = value
       })
     )
   },
 
-  getter: function(path, safe) {
+  getter: function (path, safe) {
     var parts = normalizePath(path)
     return (
       getCache.get(path) ||
       getCache.set(path, function getter(data) {
         var index = 0,
@@ -71,32 +82,32 @@ module.exports = {
         return data
       })
     )
   },
 
-  join: function(segments) {
-    return segments.reduce(function(path, part) {
+  join: function (segments) {
+    return segments.reduce(function (path, part) {
       return (
         path +
         (isQuoted(part) || DIGIT_REGEX.test(part)
           ? '[' + part + ']'
           : (path ? '.' : '') + part)
       )
     }, '')
   },
 
-  forEach: function(path, cb, thisArg) {
+  forEach: function (path, cb, thisArg) {
     forEach(Array.isArray(path) ? path : split(path), cb, thisArg)
-  }
+  },
 }
 
 function normalizePath(path) {
   return (
     pathCache.get(path) ||
     pathCache.set(
       path,
-      split(path).map(function(part) {
+      split(path).map(function (part) {
         return part.replace(CLEAN_QUOTES_REGEX, '$2')
       })
     )
   )
 }
diff --git a/test.js b/test.js
index 62494f0..195fba3 100644
--- a/test.js
+++ b/test.js
@@ -1,18 +1,20 @@
 const a = require('assert')
 const expr = require('./index')
 const compiler = require('./compiler')
 
+const root = (typeof global == 'object' && global) || this
+
 function runSetterGetterTests({ setter, getter }) {
   let obj = {
     foo: {
       bar: ['baz', 'bux'],
       fux: 5,
       '00N40000002S5U0': 1,
       N40000002S5U0: 2,
-      'FE43-D880-21AE': 3
-    }
+      'FE43-D880-21AE': 3,
+    },
   }
 
   // -- Getters --
   a.strictEqual(getter('foo.fux')(obj), 5)
   a.deepEqual(getter('foo.bar')(obj), ['baz', 'bux'])
@@ -45,11 +47,28 @@ function runSetterGetterTests({ setter, getter }) {
 
   setter('foo.bar[1]')(obj, 'bot')
   a.strictEqual(obj.foo.bar[1], 'bot')
 
   setter('[\'foo\']["bar"][1]')(obj, 'baz')
+
   a.strictEqual(obj.foo.bar[1], 'baz')
+  //
+  ;['__proto__', 'constructor', 'prototype'].forEach((keyToTest) => {
+    setter(`${keyToTest}.a`)({}, 'newValue')
+
+    a.notEqual(root['a'], 'newValue')
+
+    const b = 'oldValue'
+
+    a.equal(b, 'oldValue')
+    a.notEqual(root['b'], 'newValue')
+
+    setter(`${keyToTest}.b`)({}, 'newValue')
+    a.equal(b, 'oldValue')
+    a.notEqual(root['b'], 'newValue')
+    a.equal(root['b'], undefined)
+  })
 }
 
 console.log('--- Test Start ---')
 
 // -- Cache --
@@ -87,11 +106,11 @@ a.strictEqual(expr.join(parts), 'foo.baz["bar"][1]')
 
 // -- forEach --
 
 let count = 0
 
-expr.forEach('foo.baz["bar"][1]', function(part, isBracket, isArray, idx) {
+expr.forEach('foo.baz["bar"][1]', function (part, isBracket, isArray, idx) {
   count = idx
 
   switch (idx) {
     case 0:
       a.strictEqual(part, 'foo')

Vulnerable code:   forEach(parts, function(part, isBracket, isArray, idx, parts) {
  setter: function(path) {
  getter: function(path, safe) {
  }
Cache.prototype.clear = function() {
Cache.prototype.get = function(key) {
Cache.prototype.set = function(key, value) {
  setter: function(path) {
      setCache.set(path, function setter(data, value) {
        var index = 0,
          len = parts.length
  getter: function(path, safe) {
  join: function(segments) {
    return segments.reduce(function(path, part) {
  forEach: function(path, cb, thisArg) {
  }
      split(path).map(function(part) {
      'FE43-D880-21AE': 3
    }
expr.forEach('foo.baz["bar"][1]', function(part, isBracket, isArray, idx) {
Fixed code:   forEach(parts, function (part, isBracket, isArray, idx, parts) {
  setter: function (path) {
    if (
      path.indexOf('__proto__') !== -1 ||
      path.indexOf('constructor') !== -1 ||
      path.indexOf('prototype') !== -1
    ) {
      return (obj) => obj
    }

  getter: function (path, safe) {
  },
Cache.prototype.clear = function () {
Cache.prototype.get = function (key) {
Cache.prototype.set = function (key, value) {
  setter: function (path) {
      setCache.set(path, function setter(obj, value) {
        var index = 0
        var len = parts.length
        var data = obj

          let part = parts[index]
          if (
            part === '__proto__' ||
            part === 'constructor' ||
            part === 'prototype'
          ) {
            return obj
          }

  getter: function (path, safe) {
  join: function (segments) {
    return segments.reduce(function (path, part) {
  forEach: function (path, cb, thisArg) {
  },
      split(path).map(function (part) {
const root = (typeof global == 'object' && global) || this

      'FE43-D880-21AE': 3,
    },

  //
  ;['__proto__', 'constructor', 'prototype'].forEach((keyToTest) => {
    setter(`${keyToTest}.a`)({}, 'newValue')

    a.notEqual(root['a'], 'newValue')

    const b = 'oldValue'

    a.equal(b, 'oldValue')
    a.notEqual(root['b'], 'newValue')

    setter(`${keyToTest}.b`)({}, 'newValue')
    a.equal(b, 'oldValue')
    a.notEqual(root['b'], 'newValue')
    a.equal(root['b'], undefined)
  })
expr.forEach('foo.baz["bar"][1]', function (part, isBracket, isArray, idx) {