Prototype Pollution
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.
| Package | Affected versions | Patched versions |
|---|---|---|
mergenpm | < 2.1.1 | 2.1.1 |
Affected products
2- merge/mergedescription
Patches
16 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- github.com/advisories/GHSA-7wpw-2hjm-89gpghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2020-28499ghsaADVISORY
- github.com/yeikos/js.merge/blob/56ca75b2dd0f2820f1e08a49f62f04bbfb8c5f8f/src/index.tsghsaWEB
- github.com/yeikos/js.merge/blob/master/src/index.tsghsaWEB
- github.com/yeikos/js.merge/blob/master/src/index.ts%23L64mitrex_refsource_MISC
- github.com/yeikos/js.merge/commit/7b0ddc2701d813f2ba289b32d6a4b9d4cc235fb4ghsaWEB
- snyk.io/vuln/SNYK-JAVA-ORGWEBJARSNPM-1071049ghsax_refsource_MISCWEB
- snyk.io/vuln/SNYK-JS-MERGE-1042987ghsax_refsource_MISCWEB
- vuldb.comghsaWEB
News mentions
0No linked articles in our index yet.