VYPR
High severityNVD Advisory· Published Feb 18, 2021· Updated Sep 16, 2024

Prototype Pollution

CVE-2020-28499

Description

All versions of the npm 'merge' package are vulnerable to Prototype Pollution via the _recursiveMerge function, allowing attackers to pollute Object.prototype by injecting properties like __proto__.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

All versions of the npm 'merge' package are vulnerable to Prototype Pollution via the _recursiveMerge function, allowing attackers to pollute Object.prototype by injecting properties like __proto__.

Overview

The merge npm package (all versions before the fix) is vulnerable to Prototype Pollution, a JavaScript vulnerability that allows an attacker to inject properties into the prototype of built-in objects. The root cause lies in the _recursiveMerge function, which recursively merges properties from a source object into a target object without filtering dangerous properties. Specifically, the function iterates over all keys in the extend object using a for-in loop and directly assigns them to the base object, without checking for keys like __proto__, constructor, or prototype [1][2]. This allows a crafted object containing __proto__ to pollute Object.prototype.

Exploitation

An attacker can trigger this vulnerability by providing a JSON payload with a property named __proto__ containing arbitrary properties. For example, the payload {"__proto__": {"a": true}} would, when processed by merge.recursive, cause Object.prototype to be extended with the property a. The exploit requires no authentication and can be delivered through any input that is merged into a JavaScript object, such as API request data or configuration files. The _recursiveMerge function is called internally by the merge function, making the entire library susceptible [2][3].

Impact

Successful exploitation leads to prototype pollution, which can have severe consequences. Polluting Object.prototype means all subsequent JavaScript objects inherit the injected properties, potentially altering application behavior. This can cause denial of service (e.g., by triggering unexpected exceptions) or, more critically, enable remote code execution by forcing the application into an attacker-controlled code path. The impact is amplified because the pollution persists across the entire runtime, affecting any object that is not explicitly created via Object.create(null) [3][4].

Mitigation

The vulnerability has been patched in a commit (7b0ddc270) that adds a guard in the _recursiveMerge function to skip keys that are __proto__, constructor, or prototype. Users of the merge package should update to a patched version (if available) or apply the fix manually by adjusting the source code per the committed diff [2]. At the time of publication, no workaround exists other than avoiding the use of recursive merging with untrusted data or switching to a different merge library.

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.

PackageAffected versionsPatched versions
mergenpm
< 2.1.12.1.1

Affected products

2

Patches

1
7b0ddc2701d8

fix: prototype pollution

https://github.com/yeikos/js.mergeyeikosFeb 22, 2021via ghsa
6 files changed · +27 4
  • dist/merge.browser.min.js+1 1 modified
    @@ -1 +1 @@
    -var merge;merge=(()=>{"use strict";var r={496:(r,e)=>{function t(){for(var r=[],e=0;e<arguments.length;e++)r[e]=arguments[e];return n.apply(void 0,r)}function n(){for(var r=[],e=0;e<arguments.length;e++)r[e]=arguments[e];return f(!0===r[0],!1,r)}function o(){for(var r=[],e=0;e<arguments.length;e++)r[e]=arguments[e];return f(!0===r[0],!0,r)}function i(r){if(Array.isArray(r)){for(var e=[],t=0;t<r.length;++t)e.push(i(r[t]));return e}if(u(r)){for(var t in e={},r)e[t]=i(r[t]);return e}return r}function u(r){return r&&"object"==typeof r&&!Array.isArray(r)}function a(r,e){if(!u(r))return e;for(var t in e)r[t]=u(r[t])&&u(e[t])?a(r[t],e[t]):e[t];return r}function f(r,e,t){var n;!r&&u(n=t.shift())||(n={});for(var o=0;o<t.length;++o){var f=t[o];if(u(f))for(var c in f)if("__proto__"!==c&&"constructor"!==c&&"prototype"!==c){var v=r?i(f[c]):f[c];n[c]=e?a(n[c],v):v}}return n}Object.defineProperty(e,"__esModule",{value:!0}),e.isPlainObject=e.clone=e.recursive=e.merge=e.main=void 0,r.exports=e=t,e.default=t,e.main=t,t.clone=i,t.isPlainObject=u,t.recursive=o,e.merge=n,e.recursive=o,e.clone=i,e.isPlainObject=u}},e={};return function t(n){if(e[n])return e[n].exports;var o=e[n]={exports:{}};return r[n](o,o.exports,t),o.exports}(496)})().default;
    \ No newline at end of file
    +var merge;merge=(()=>{"use strict";var r={496:(r,e)=>{function t(){for(var r=[],e=0;e<arguments.length;e++)r[e]=arguments[e];return n.apply(void 0,r)}function n(){for(var r=[],e=0;e<arguments.length;e++)r[e]=arguments[e];return f(!0===r[0],!1,r)}function o(){for(var r=[],e=0;e<arguments.length;e++)r[e]=arguments[e];return f(!0===r[0],!0,r)}function i(r){if(Array.isArray(r)){for(var e=[],t=0;t<r.length;++t)e.push(i(r[t]));return e}if(u(r)){for(var t in e={},r)e[t]=i(r[t]);return e}return r}function u(r){return r&&"object"==typeof r&&!Array.isArray(r)}function a(r,e){if(!u(r))return e;for(var t in e)"__proto__"!==t&&"constructor"!==t&&"prototype"!==t&&(r[t]=u(r[t])&&u(e[t])?a(r[t],e[t]):e[t]);return r}function f(r,e,t){var n;!r&&u(n=t.shift())||(n={});for(var o=0;o<t.length;++o){var f=t[o];if(u(f))for(var c in f)if("__proto__"!==c&&"constructor"!==c&&"prototype"!==c){var s=r?i(f[c]):f[c];n[c]=e?a(n[c],s):s}}return n}Object.defineProperty(e,"__esModule",{value:!0}),e.isPlainObject=e.clone=e.recursive=e.merge=e.main=void 0,r.exports=e=t,e.default=t,e.main=t,t.clone=i,t.isPlainObject=u,t.recursive=o,e.merge=n,e.recursive=o,e.clone=i,e.isPlainObject=u}},e={};return function t(n){if(e[n])return e[n].exports;var o=e[n]={exports:{}};return r[n](o,o.exports,t),o.exports}(496)})().default;
    \ No newline at end of file
    
  • dist/merge.browser.test.js+7 1 modified
    @@ -62,10 +62,13 @@ exports.isPlainObject = isPlainObject;
     function _recursiveMerge(base, extend) {
         if (!isPlainObject(base))
             return extend;
    -    for (var key in extend)
    +    for (var key in extend) {
    +        if (key === '__proto__' || key === 'constructor' || key === 'prototype')
    +            continue;
             base[key] = (isPlainObject(base[key]) && isPlainObject(extend[key])) ?
                 _recursiveMerge(base[key], extend[key]) :
                 extend[key];
    +    }
         return base;
     }
     function _merge(isClone, isRecursive, items) {
    @@ -194,6 +197,9 @@ describe('merge.recursive', function () {
         });
         it('prototype pollution attack', function () {
             chai_1.assert.deepEqual(index_1.default.recursive({}, JSON.parse('{"__proto__": {"a": true}}')), {});
    +        chai_1.assert.equal({}['a'], undefined);
    +        chai_1.assert.deepEqual(index_1.default.recursive({ deep: {} }, JSON.parse('{ "deep": { "__proto__": {"b": true} }}')), { deep: {} });
    +        chai_1.assert.equal({}['b'], undefined);
         });
     });
     
    
  • lib/src/index.js+4 1 modified
    @@ -55,10 +55,13 @@ exports.isPlainObject = isPlainObject;
     function _recursiveMerge(base, extend) {
         if (!isPlainObject(base))
             return extend;
    -    for (var key in extend)
    +    for (var key in extend) {
    +        if (key === '__proto__' || key === 'constructor' || key === 'prototype')
    +            continue;
             base[key] = (isPlainObject(base[key]) && isPlainObject(extend[key])) ?
                 _recursiveMerge(base[key], extend[key]) :
                 extend[key];
    +    }
         return base;
     }
     function _merge(isClone, isRecursive, items) {
    
  • lib/test/index.js+3 0 modified
    @@ -99,5 +99,8 @@ describe('merge.recursive', function () {
         });
         it('prototype pollution attack', function () {
             chai_1.assert.deepEqual(index_1.default.recursive({}, JSON.parse('{"__proto__": {"a": true}}')), {});
    +        chai_1.assert.equal({}['a'], undefined);
    +        chai_1.assert.deepEqual(index_1.default.recursive({ deep: {} }, JSON.parse('{ "deep": { "__proto__": {"b": true} }}')), { deep: {} });
    +        chai_1.assert.equal({}['b'], undefined);
         });
     });
    
  • src/index.ts+3 1 modified
    @@ -61,10 +61,12 @@ function _recursiveMerge(base: any, extend: any) {
     	if (!isPlainObject(base))
     		return extend
     
    -	for (const key in extend)
    +	for (const key in extend) {
    +		if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue
     		base[key] = (isPlainObject(base[key]) && isPlainObject(extend[key])) ?
     			_recursiveMerge(base[key], extend[key]) :
     			extend[key]
    +	}
     
     	return base
     
    
  • test/index.ts+9 0 modified
    @@ -219,6 +219,15 @@ describe('merge.recursive', function () {
     			{}
     		)
     
    +		assert.equal(({} as any)['a'], undefined)
    +
    +		assert.deepEqual(
    +			merge.recursive({ deep: {} }, JSON.parse('{ "deep": { "__proto__": {"b": true} }}')),
    +			{ deep: {} }
    +		)
    +
    +		assert.equal(({} as any)['b'], undefined)
    +
     	})
     
     })
    \ No newline at end of file
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

9

News mentions

0

No linked articles in our index yet.