Prototype Pollution
Description
The property-expr package before 2.0.3 is vulnerable to Prototype Pollution via the setter function, allowing attackers to pollute Object.prototype.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
The property-expr package before 2.0.3 is vulnerable to Prototype Pollution via the setter function, allowing attackers to pollute Object.prototype.
Overview
CVE-2020-7707 is a Prototype Pollution vulnerability in the property-expr npm package versions before 2.0.3. The root cause lies in the setter function, which constructs a path to set a property on an object without sanitizing the input for dangerous property names such as __proto__, constructor, or prototype [1]. This allows an attacker to inject properties into the global Object.prototype.
Exploitation
The vulnerability can be exploited by providing a crafted path string to the setter function. For example, a path like __proto__.polluted would cause the function to set a property on Object.prototype [1]. This is a classic "property definition by path" style of Prototype Pollution [3][4]. The attack requires no authentication and can be triggered if the application uses user-controlled input as part of the path.
Impact
Successful exploitation leads to Prototype Pollution, where properties are injected into the base Object.prototype. This can result in unexpected behavior, denial of service, or remote code execution depending on how the application uses object properties [3]. The vulnerability can also be used to bypass security checks or tamper with application logic.
Mitigation
The issue was fixed in version 2.0.3 of property-expr by adding checks that reject paths containing __proto__, constructor, or prototype [1]. Users are advised to upgrade to at least version 2.0.3. No workaround is available other than updating the package.
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
property-exprnpm | < 2.0.3 | 2.0.3 |
Affected products
2- property-expr/property-exprdescription
Patches
1df846910915dfix: prototype polution vector
3 files changed · +58 −20
compiler.js+12 −4 modified@@ -8,7 +8,7 @@ function makeSafe(path, 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 @@ -36,7 +36,15 @@ function expr(expression, safe, param) { 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( @@ -46,7 +54,7 @@ module.exports = { ) }, - getter: function(path, safe) { + getter: function (path, safe) { var key = path + '_' + safe return ( getCache.get(key) || @@ -55,5 +63,5 @@ module.exports = { new Function('data', 'return ' + expr(path, safe, 'data')) ) ) - } + }, }
index.js+24 −13 modified@@ -7,14 +7,14 @@ 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++ @@ -41,23 +41,34 @@ module.exports = { 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) || @@ -73,8 +84,8 @@ module.exports = { ) }, - 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) @@ -84,17 +95,17 @@ module.exports = { }, '') }, - 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') }) )
test.js+22 −3 modified@@ -2,15 +2,17 @@ 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 -- @@ -47,7 +49,24 @@ function runSetterGetterTests({ setter, getter }) { 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 ---') @@ -89,7 +108,7 @@ a.strictEqual(expr.join(parts), 'foo.baz["bar"][1]') 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) {
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-6fw4-hr69-g3rvghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2020-7707ghsaADVISORY
- github.com/jquense/expr/commit/df846910915d59f711ce63c1f817815bceab5ff7ghsax_refsource_MISCWEB
- snyk.io/vuln/SNYK-JAVA-ORGWEBJARSNPM-598857ghsax_refsource_MISCWEB
- snyk.io/vuln/SNYK-JS-PROPERTYEXPR-598800ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.