Critical severity9.8NVD Advisory· Published Mar 6, 2026· Updated Apr 17, 2026
CVE-2026-29063
CVE-2026-29063
Description
Immutable.js provides many Persistent Immutable data structures. Prior to versions 3.8.3, 4.3.7, and 5.1.5, Prototype Pollution is possible in immutable via the mergeDeep(), mergeDeepWith(), merge(), Map.toJS(), and Map.toObject() APIs. This issue has been patched in versions 3.8.3, 4.3.7, and 5.1.5.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
immutablenpm | >= 4.0.0-rc.1, < 4.3.8 | 4.3.8 |
immutablenpm | >= 5.0.0, < 5.1.5 | 5.1.5 |
immutablenpm | < 3.8.3 | 3.8.3 |
Affected products
1Patches
36e2cf1cfe613Port patch for CVE 2026-29063 onto branch 3.x
7 files changed · +117 −30
dist/immutable.js+13 −1 modified@@ -4246,6 +4246,12 @@ return ctor; } + function isProtoKey(key) { + return ( + typeof key === 'string' && (key === '__proto__' || key === 'constructor') + ); + } + Iterable.Iterator = Iterator; mixin(Iterable, { @@ -4287,7 +4293,13 @@ toObject: function() { assertNotInfinite(this.size); var object = {}; - this.__iterate(function(v, k) { object[k] = v; }); + this.__iterate(function(v, k) { + if (isProtoKey(k)) { + return; + } + + object[k] = v; + }); return object; },
dist/immutable.min.js+28 −28 modifiedsrc/IterableImpl.js+8 −1 modified@@ -20,6 +20,7 @@ import { Iterator, ITERATOR_SYMBOL, ITERATE_KEYS, ITERATE_VALUES, ITERATE_ENTRIES } from './Iterator' import assertNotInfinite from './utils/assertNotInfinite' +import { isProtoKey } from './utils/protoInjection' import forceIterator from './utils/forceIterator' import deepEqual from './utils/deepEqual' import mixin from './utils/mixin' @@ -85,7 +86,13 @@ mixin(Iterable, { toObject() { assertNotInfinite(this.size); var object = {}; - this.__iterate((v, k) => { object[k] = v; }); + this.__iterate((v, k) => { + if (isProtoKey(k)) { + return; + } + + object[k] = v; + }); return object; },
src/utils/protoInjection.js+5 −0 added@@ -0,0 +1,5 @@ +export function isProtoKey(key) { + return ( + typeof key === 'string' && (key === '__proto__' || key === 'constructor') + ); +}
__tests__/Map.ts+5 −0 modified@@ -353,4 +353,9 @@ describe('Map', () => { expect(is(m1, m2)).toBe(true); }); + it('toJS / toObject are not sensible to prototype pollution', () => { + var m = (Map({ user: 'alice' }) as any).set('__proto__', Map({ admin: true })); + expect(m.toObject().admin).toBeUndefined(); + expect(m.toJS().admin).toBeUndefined(); + }); });
__tests__/merge.ts+37 −0 modified@@ -149,4 +149,41 @@ describe('merge', () => { expect(m2.getIn(['a', 'b', 0])).toEqual({plain: 'obj'}); }) + it('is not sensible to prototype pollution', () => { + var m1 = fromJS({user: 'Alice'}); + // Map().set('__proto__', ...) properly creates a __proto__ key in the Map + // (unlike Map({ __proto__: ... }) which triggers JS prototype setter) + var m2 = Map().set('__proto__', Map({ admin: true })); + + var r1 = m1.mergeDeep(m2); + // @ts-ignore -- testing prototype pollution, ignoring typing errors for tests + expect(r1.toJS().admin).toBeUndefined(); + + var r2 = m1.mergeDeepWith((a, b) => b, m2); + // @ts-ignore -- testing prototype pollution, ignoring typing errors for tests + expect(r2.toJS().admin).toBeUndefined(); + + var r3 = m1.merge(m2); + // @ts-ignore -- testing prototype pollution, ignoring typing errors for tests + expect(r3.toJS().admin).toBeUndefined(); + + // @ts-ignore -- testing prototype pollution, ignoring typing errors for tests + expect((({}) as any).admin).toBeUndefined(); + }) + + it('is not sensible to prototype pollution via fromJS + JSON.parse', () => { + var userProfile = fromJS({user: 'Alice'}); + var requestBody = fromJS(JSON.parse('{"user":"Eve","__proto__":{"admin":true}}')); + + var r1 = userProfile.mergeDeep(requestBody); + expect(r1.get('user')).toBe('Eve'); + // @ts-ignore -- testing prototype pollution, ignoring typing errors for tests + expect(r1.toJS().admin).toBeUndefined(); + // @ts-ignore -- testing prototype pollution, ignoring typing errors for tests + expect(r1.toObject().admin).toBeUndefined(); + + // @ts-ignore -- testing prototype pollution, ignoring typing errors for tests + expect((({}) as any).admin).toBeUndefined(); + }) + })
__tests__/updateIn.ts+21 −0 modified@@ -278,4 +278,25 @@ describe('updateIn', () => { }) + describe('prototype pollution', () => { + it('setIn on Map with __proto__ key should not pollute toObject result', () => { + var m = Map({profile: Map({bio: 'Hello'})}) as any; + var result = m.setIn(['__proto__', 'admin'], true); + expect(result.toObject().admin).toBeUndefined(); + }) + + it('setIn on Map with nested __proto__ key should not pollute toJS result', () => { + var m = Map({profile: Map({bio: 'Hello'})}) as any; + var result = m.setIn(['profile', '__proto__', 'admin'], true); + expect(result.toJS().profile.admin).toBeUndefined(); + }) + + it('updateIn on Map with __proto__ key should not pollute toObject result', () => { + var m = Map({profile: Map({bio: 'Hello'})}) as any; + // @ts-ignore -- this is testing that we don't allow __proto__ to be used as a key, so we need to bypass the type system. + var result = m.updateIn(['__proto__', 'admin'], () => true); + expect(result.toObject().admin).toBeUndefined(); + }) + }) + })
6ed4eb626906Merge commit from fork
12 files changed · +290 −0
src/functional/merge.js+5 −0 modified@@ -5,6 +5,7 @@ import { IndexedCollection, KeyedCollection } from '../Collection'; import { Seq } from '../Seq'; import hasOwnProperty from '../utils/hasOwnProperty'; import isDataStructure from '../utils/isDataStructure'; +import { isProtoKey } from '../utils/protoInjection'; import shallowCopy from '../utils/shallowCopy'; export function merge(collection, ...sources) { @@ -52,6 +53,10 @@ export function mergeWithSources(collection, sources, merger) { merged.push(value); } : (value, key) => { + if (isProtoKey(key)) { + return; + } + const hasVal = hasOwnProperty.call(merged, key); const nextVal = hasVal && merger ? merger(merged[key], value, key) : value;
src/functional/set.js+5 −0 modified@@ -1,9 +1,14 @@ import { isImmutable } from '../predicates/isImmutable'; import hasOwnProperty from '../utils/hasOwnProperty'; import isDataStructure from '../utils/isDataStructure'; +import { isProtoKey } from '../utils/protoInjection'; import shallowCopy from '../utils/shallowCopy'; export function set(collection, key, value) { + if (isProtoKey(key)) { + return collection; + } + if (!isDataStructure(collection)) { throw new TypeError( 'Cannot update non-data-structure value: ' + collection
src/methods/toObject.js+5 −0 modified@@ -1,9 +1,14 @@ import assertNotInfinite from '../utils/assertNotInfinite'; +import { isProtoKey } from '../utils/protoInjection'; export function toObject() { assertNotInfinite(this.size); const object = {}; this.__iterate((v, k) => { + if (isProtoKey(k)) { + return; + } + object[k] = v; }); return object;
src/toJS.js+5 −0 modified@@ -2,6 +2,7 @@ import { Seq } from './Seq'; import { isCollection } from './predicates/isCollection'; import { isKeyed } from './predicates/isKeyed'; import isDataStructure from './utils/isDataStructure'; +import { isProtoKey } from './utils/protoInjection'; export function toJS(value) { if (!value || typeof value !== 'object') { @@ -16,6 +17,10 @@ export function toJS(value) { if (isKeyed(value)) { const result = {}; value.__iterate((v, k) => { + if (isProtoKey(k)) { + return; + } + result[k] = toJS(v); }); return result;
src/utils/protoInjection.js+5 −0 added@@ -0,0 +1,5 @@ +export function isProtoKey(key) { + return ( + typeof key === 'string' && (key === '__proto__' || key === 'constructor') + ); +}
src/utils/shallowCopy.js+5 −0 modified@@ -1,12 +1,17 @@ import arrCopy from './arrCopy'; import hasOwnProperty from './hasOwnProperty'; +import { isProtoKey } from './protoInjection'; export default function shallowCopy(from) { if (Array.isArray(from)) { return arrCopy(from); } const to = {}; for (const key in from) { + if (isProtoKey(key)) { + continue; + } + if (hasOwnProperty.call(from, key)) { to[key] = from[key]; }
__tests__/functional/set.ts+69 −0 added@@ -0,0 +1,69 @@ +import { describe, expect, it } from '@jest/globals'; +import { set } from 'immutable'; + +describe('set', () => { + it('for immutable structure', () => { + const originalArray = ['dog', 'frog', 'cat']; + expect(set(originalArray, 1, 'cow')).toEqual(['dog', 'cow', 'cat']); + expect(set(originalArray, 4, 'cow')).toEqual([ + 'dog', + 'frog', + 'cat', + undefined, + 'cow', + ]); + expect(originalArray).toEqual(['dog', 'frog', 'cat']); + + const originalObject = { x: 123, y: 456 }; + expect(set(originalObject, 'x', 789)).toEqual({ x: 789, y: 456 }); + expect(set(originalObject, 'z', 789)).toEqual({ x: 123, y: 456, z: 789 }); + expect(originalObject).toEqual({ x: 123, y: 456 }); + }); + + it('for Array', () => { + const originalArray = ['dog', 'frog', 'cat']; + expect(set(originalArray, 1, 'cow')).toEqual(['dog', 'cow', 'cat']); + expect(set(originalArray, 4, 'cow')).toEqual([ + 'dog', + 'frog', + 'cat', + undefined, + 'cow', + ]); + expect(originalArray).toEqual(['dog', 'frog', 'cat']); + }); + + it('for plain objects', () => { + const originalObject = { x: 123, y: 456 }; + expect(set(originalObject, 'x', 789)).toEqual({ x: 789, y: 456 }); + expect(set(originalObject, 'z', 789)).toEqual({ x: 123, y: 456, z: 789 }); + expect(originalObject).toEqual({ x: 123, y: 456 }); + }); + + it('is not sensible to prototype pollution via set on plain object', () => { + type User = { user: string; admin?: boolean }; + + const obj: User = { user: 'Alice' }; + // Setting __proto__ key should not change the returned object's prototype chain + // @ts-expect-error -- intentionally setting __proto__ to test prototype pollution + const result = set(obj, '__proto__', { admin: true }); + + // The returned copy should NOT have 'admin' accessible via prototype + // @ts-expect-error -- testing prototype pollution + expect(result.admin).toBeUndefined(); + }); + + it('is not sensible to prototype pollution via set with JSON.parse source', () => { + type User = { user: string; admin?: boolean }; + + // JSON.parse creates __proto__ as an own property + const malicious = JSON.parse( + '{"user":"Eve","__proto__":{"admin":true}}' + ) as User; + // set on an object that already carries __proto__ from JSON.parse + const result = set(malicious, 'user', 'Alice'); + + // The returned copy should NOT have 'admin' accessible via prototype pollution + expect(result.admin).toBeUndefined(); + }); +});
__tests__/functional/update.ts+64 −0 added@@ -0,0 +1,64 @@ +import { describe, expect, it } from '@jest/globals'; +import { update } from 'immutable'; + +describe('update', () => { + it('for immutable structure', () => { + const originalArray = ['dog', 'frog', 'cat']; + expect(update(originalArray, 1, val => val?.toUpperCase())).toEqual([ + 'dog', + 'FROG', + 'cat', + ]); + expect(originalArray).toEqual(['dog', 'frog', 'cat']); + + const originalObject = { x: 123, y: 456 }; + expect(update(originalObject, 'x', val => val * 6)).toEqual({ + x: 738, + y: 456, + }); + expect(originalObject).toEqual({ x: 123, y: 456 }); + }); + + it('for Array', () => { + const originalArray = ['dog', 'frog', 'cat']; + expect(update(originalArray, 1, val => val?.toUpperCase())).toEqual([ + 'dog', + 'FROG', + 'cat', + ]); + expect(originalArray).toEqual(['dog', 'frog', 'cat']); + }); + + it('for plain objects', () => { + const originalObject = { x: 123, y: 456 }; + expect(update(originalObject, 'x', val => val * 6)).toEqual({ + x: 738, + y: 456, + }); + expect(originalObject).toEqual({ x: 123, y: 456 }); + }); + + it('is not sensible to prototype pollution via update on plain object', () => { + type User = { user: string; admin?: boolean }; + + const obj: User = { user: 'Alice' }; + // @ts-expect-error -- intentionally setting __proto__ to test prototype pollution + const result = update(obj, '__proto__', () => ({ + admin: true, + })) as unknown as User; + + // The returned copy should NOT have 'admin' accessible via prototype + expect(result.admin).toBeUndefined(); + }); + + it('is not sensible to prototype pollution via update with JSON.parse source', () => { + type User = { user: string; admin?: boolean }; + + // JSON.parse creates __proto__ as an own property + const malicious = JSON.parse('{"user":"Eve","__proto__":{"admin":true}}'); + const result = update(malicious, 'user', () => 'Alice') as User; + + // The returned copy (via shallowCopy) should NOT have 'admin' via prototype + expect(result.admin).toBeUndefined(); + }); +});
__tests__/Map.ts+9 −0 modified@@ -578,4 +578,13 @@ describe('Map', () => { ]) ); }); + + it('toJS / toObject are not sensible to prototype pollution', () => { + type User = { user: string; admin?: boolean }; + + // @ts-expect-error -- intentionally setting __proto__ to test prototype pollution + const m = Map<User>({ user: 'alice' }).set('__proto__', { admin: true }); + expect(m.toObject().admin).toBeUndefined(); + expect(m.toJS().admin).toBeUndefined(); + }); });
__tests__/merge.ts+30 −0 modified@@ -330,4 +330,34 @@ describe('merge', () => { const b = Map({ a: Map([[0, Map({ y: 2 })]]) }); expect(mergeDeep(a, b)).toEqual({ a: Map([[0, Map({ y: 2 })]]) }); }); + + it('is not sensible to prototype pollution', () => { + type User = { user: string; admin?: boolean }; + + // Simulates: app merges HTTP request body (JSON) into user profile + const userProfile: User = { user: 'Alice' }; + const requestBody = JSON.parse('{"user":"Eve","__proto__":{"admin":true}}'); + + const r1 = mergeDeep(userProfile, requestBody); + + expect(r1.user).toBe('Eve'); // Eve (updated correctly) + expect(r1.admin).toBeUndefined(); + + const r2 = mergeDeepWith((a, b) => b, userProfile, requestBody); + expect(r2.admin).toBeUndefined(); + + const r3 = merge(userProfile, requestBody); + expect(r3.admin).toBeUndefined(); + + const nested = JSON.parse('{"profile":{"__proto__":{"admin":true}}}'); + const r6 = mergeDeep<{ profile: { bio: string; admin?: boolean } }>( + { profile: { bio: 'Hello' } }, + nested + ); + + expect(r6.profile.admin).toBeUndefined(); + + // @ts-expect-error -- testing prototype pollution + expect({}.admin).toBeUndefined(); // Confirm NOT global too + }); });
__tests__/updateIn.ts+32 −0 modified@@ -368,4 +368,36 @@ describe('updateIn', () => { ); }); }); + + describe('prototype pollution', () => { + it('setIn on plain object with __proto__ key should not pollute returned object', () => { + type User = { profile: { bio: string }; admin?: boolean }; + + const obj: User = { profile: { bio: 'Hello' } }; + const result = setIn(obj, ['__proto__', 'admin'], true); + + // The returned object should NOT have 'admin' accessible via prototype + expect(result.admin).toBeUndefined(); + }); + + it('setIn on plain object with nested __proto__ key should not pollute returned object', () => { + type User = { profile: { bio: string; admin?: boolean } }; + + const obj: User = { profile: { bio: 'Hello' } }; + const result = setIn(obj, ['profile', '__proto__', 'admin'], true); + + // The nested object should NOT have 'admin' accessible via prototype + expect(result.profile.admin).toBeUndefined(); + }); + + it('updateIn on plain object with __proto__ key should not pollute returned object', () => { + type User = { profile: { bio: string }; admin?: boolean }; + + const obj: User = { profile: { bio: 'Hello' } }; + const result = updateIn(obj, ['__proto__', 'admin'], () => true); + + // The returned object should NOT have 'admin' accessible via prototype + expect(result.admin).toBeUndefined(); + }); + }); });
__tests__/utils/shallowCopy.ts+56 −0 added@@ -0,0 +1,56 @@ +import { describe, it, expect } from '@jest/globals'; +import shallowCopy from '../../src/utils/shallowCopy'; + +describe('shallowCopy', () => { + it('copies a plain object', () => { + const obj = { a: 1, b: 2 }; + const copy = shallowCopy(obj); + expect(copy).toEqual({ a: 1, b: 2 }); + expect(copy).not.toBe(obj); + }); + + it('copies an array', () => { + const arr = [1, 2, 3]; + const copy = shallowCopy(arr); + expect(copy).toEqual([1, 2, 3]); + expect(copy).not.toBe(arr); + }); + + it('should not propagate __proto__ key from source object', () => { + type User = { user: string; admin?: boolean }; + + // @ts-expect-error -- testing prototype pollution + delete Object.prototype.admin; + + // JSON.parse creates an own property named "__proto__" (not the actual prototype) + const malicious = JSON.parse('{"user":"Eve","__proto__":{"admin":true}}'); + + const copy = shallowCopy(malicious); + + // The copy should NOT have admin on its prototype chain + expect((copy as User).admin).toBeUndefined(); + + // Global Object prototype should NOT be polluted + expect(({} as User).admin).toBeUndefined(); + + // @ts-expect-error -- cleanup + delete Object.prototype.admin; + }); + + it('should not propagate constructor key from source object', () => { + type User = { user: string; admin?: boolean }; + + const malicious: User = { + user: 'Eve', + // @ts-expect-error -- intentionally setting constructor to test pollution + constructor: { prototype: { admin: true } }, + }; + + const copy = shallowCopy(malicious); + + expect((copy as User).admin).toBeUndefined(); + + // The constructor of a plain new object should still be Object + expect({}.constructor).toBe(Object); + }); +});
16b3313fdf2cMerge commit from fork
12 files changed · +207 −0
src/functional/merge.js+5 −0 modified@@ -5,6 +5,7 @@ import { isIndexed } from '../predicates/isIndexed'; import { isKeyed } from '../predicates/isKeyed'; import hasOwnProperty from '../utils/hasOwnProperty'; import isDataStructure from '../utils/isDataStructure'; +import { isProtoKey } from '../utils/protoInjection'; import shallowCopy from '../utils/shallowCopy'; export function merge(collection, ...sources) { @@ -52,6 +53,10 @@ export function mergeWithSources(collection, sources, merger) { merged.push(value); } : (value, key) => { + if (isProtoKey(key)) { + return; + } + const hasVal = hasOwnProperty.call(merged, key); const nextVal = hasVal && merger ? merger(merged[key], value, key) : value;
src/functional/set.ts+5 −0 modified@@ -2,6 +2,7 @@ import type { Collection, Record } from '../../type-definitions/immutable'; import { isImmutable } from '../predicates/isImmutable'; import hasOwnProperty from '../utils/hasOwnProperty'; import isDataStructure from '../utils/isDataStructure'; +import { isProtoKey } from '../utils/protoInjection'; import shallowCopy from '../utils/shallowCopy'; /** @@ -38,6 +39,10 @@ export function set<K, V, C extends Collection<K, V> | { [key: string]: V }>( key: K | string, value: V ): C { + if (typeof key === 'string' && isProtoKey(key)) { + return collection; + } + if (!isDataStructure(collection)) { throw new TypeError( 'Cannot update non-data-structure value: ' + collection
src/methods/toObject.js+5 −0 modified@@ -1,9 +1,14 @@ import assertNotInfinite from '../utils/assertNotInfinite'; +import { isProtoKey } from '../utils/protoInjection'; export function toObject() { assertNotInfinite(this.size); const object = {}; this.__iterate((v, k) => { + if (isProtoKey(k)) { + return; + } + object[k] = v; }); return object;
src/toJS.ts+5 −0 modified@@ -4,6 +4,7 @@ import { Seq } from './Seq'; import { isCollection } from './predicates/isCollection'; import { isKeyed } from './predicates/isKeyed'; import isDataStructure from './utils/isDataStructure'; +import { isProtoKey } from './utils/protoInjection'; export function toJS( value: Collection | Record @@ -26,6 +27,10 @@ export function toJS( const result: { [key: string]: unknown } = {}; // @ts-expect-error `__iterate` exists on all Keyed collections but method is not defined in the type value.__iterate((v, k) => { + if (isProtoKey(k)) { + return; + } + result[k] = toJS(v); }); return result;
src/utils/protoInjection.ts+5 −0 added@@ -0,0 +1,5 @@ +export function isProtoKey(key: unknown): boolean { + return ( + typeof key === 'string' && (key === '__proto__' || key === 'constructor') + ); +}
src/utils/shallowCopy.ts+5 −0 modified@@ -1,5 +1,6 @@ import arrCopy from './arrCopy'; import hasOwnProperty from './hasOwnProperty'; +import { isProtoKey } from './protoInjection'; export default function shallowCopy<I>(from: Array<I>): Array<I>; export default function shallowCopy<O extends object>(from: O): O; @@ -11,6 +12,10 @@ export default function shallowCopy<I, O extends object>( } const to: Partial<O> = {}; for (const key in from) { + if (isProtoKey(key)) { + continue; + } + if (hasOwnProperty.call(from, key)) { to[key] = from[key]; }
__tests__/functional/set.ts+27 −0 modified@@ -39,4 +39,31 @@ describe('set', () => { expect(set(originalObject, 'z', 789)).toEqual({ x: 123, y: 456, z: 789 }); expect(originalObject).toEqual({ x: 123, y: 456 }); }); + + it('is not sensible to prototype pollution via set on plain object', () => { + type User = { user: string; admin?: boolean }; + + const obj: User = { user: 'Alice' }; + // Setting __proto__ key should not change the returned object's prototype chain + // @ts-expect-error -- intentionally setting __proto__ to test prototype pollution + const result = set(obj, '__proto__', { admin: true }); + + // The returned copy should NOT have 'admin' accessible via prototype + // @ts-expect-error -- testing prototype pollution + expect(result.admin).toBeUndefined(); + }); + + it('is not sensible to prototype pollution via set with JSON.parse source', () => { + type User = { user: string; admin?: boolean }; + + // JSON.parse creates __proto__ as an own property + const malicious = JSON.parse( + '{"user":"Eve","__proto__":{"admin":true}}' + ) as User; + // set on an object that already carries __proto__ from JSON.parse + const result = set(malicious, 'user', 'Alice'); + + // The returned copy should NOT have 'admin' accessible via prototype pollution + expect(result.admin).toBeUndefined(); + }); });
__tests__/functional/update.ts+24 −0 modified@@ -37,4 +37,28 @@ describe('update', () => { }); expect(originalObject).toEqual({ x: 123, y: 456 }); }); + + it('is not sensible to prototype pollution via update on plain object', () => { + type User = { user: string; admin?: boolean }; + + const obj: User = { user: 'Alice' }; + // @ts-expect-error -- intentionally setting __proto__ to test prototype pollution + const result = update(obj, '__proto__', () => ({ + admin: true, + })) as unknown as User; + + // The returned copy should NOT have 'admin' accessible via prototype + expect(result.admin).toBeUndefined(); + }); + + it('is not sensible to prototype pollution via update with JSON.parse source', () => { + type User = { user: string; admin?: boolean }; + + // JSON.parse creates __proto__ as an own property + const malicious = JSON.parse('{"user":"Eve","__proto__":{"admin":true}}'); + const result = update(malicious, 'user', () => 'Alice') as User; + + // The returned copy (via shallowCopy) should NOT have 'admin' via prototype + expect(result.admin).toBeUndefined(); + }); });
__tests__/Map.ts+9 −0 modified@@ -590,4 +590,13 @@ describe('Map', () => { ]) ); }); + + it('toJS / toObject are not sensible to prototype pollution', () => { + type User = { user: string; admin?: boolean }; + + // @ts-expect-error -- intentionally setting __proto__ to test prototype pollution + const m = Map<User>({ user: 'alice' }).set('__proto__', { admin: true }); + expect(m.toObject().admin).toBeUndefined(); + expect(m.toJS().admin).toBeUndefined(); + }); });
__tests__/merge.ts+29 −0 modified@@ -349,4 +349,33 @@ describe('merge', () => { // merging with an empty record should return the same empty record instance expect(merge(myEmptyRecord, { a: 4 })).toBe(myEmptyRecord); }); + + it('is not sensible to prototype pollution', () => { + type User = { user: string; admin?: boolean }; + + // Simulates: app merges HTTP request body (JSON) into user profile + const userProfile: User = { user: 'Alice' }; + const requestBody = JSON.parse('{"user":"Eve","__proto__":{"admin":true}}'); + + const r1 = mergeDeep(userProfile, requestBody); + + expect(r1.user).toBe('Eve'); // Eve (updated correctly) + expect(r1.admin).toBeUndefined(); + + const r2 = mergeDeepWith((a, b) => b, userProfile, requestBody); + expect(r2.admin).toBeUndefined(); + + const r3 = merge(userProfile, requestBody); + expect(r3.admin).toBeUndefined(); + + const nested = JSON.parse('{"profile":{"__proto__":{"admin":true}}}'); + const r6 = mergeDeep<{ profile: { bio: string; admin?: boolean } }>( + { profile: { bio: 'Hello' } }, + nested + ); + + expect(r6.profile.admin).toBeUndefined(); + + expect({}.admin).toBeUndefined(); // Confirm NOT global too + }); });
__tests__/updateIn.ts+32 −0 modified@@ -458,4 +458,36 @@ describe('updateIn', () => { ); }); }); + + describe('prototype pollution', () => { + it('setIn on plain object with __proto__ key should not pollute returned object', () => { + type User = { profile: { bio: string }; admin?: boolean }; + + const obj: User = { profile: { bio: 'Hello' } }; + const result = setIn(obj, ['__proto__', 'admin'], true); + + // The returned object should NOT have 'admin' accessible via prototype + expect(result.admin).toBeUndefined(); + }); + + it('setIn on plain object with nested __proto__ key should not pollute returned object', () => { + type User = { profile: { bio: string }; admin?: boolean }; + + const obj: User = { profile: { bio: 'Hello' } }; + const result = setIn(obj, ['profile', '__proto__', 'admin'], true); + + // The nested object should NOT have 'admin' accessible via prototype + expect(result.profile.admin).toBeUndefined(); + }); + + it('updateIn on plain object with __proto__ key should not pollute returned object', () => { + type User = { profile: { bio: string }; admin?: boolean }; + + const obj: User = { profile: { bio: 'Hello' } }; + const result = updateIn(obj, ['__proto__', 'admin'], () => true); + + // The returned object should NOT have 'admin' accessible via prototype + expect(result.admin).toBeUndefined(); + }); + }); });
__tests__/utils/shallowCopy.ts+56 −0 added@@ -0,0 +1,56 @@ +import { describe, it, expect } from '@jest/globals'; +import shallowCopy from '../../src/utils/shallowCopy'; + +describe('shallowCopy', () => { + it('copies a plain object', () => { + const obj = { a: 1, b: 2 }; + const copy = shallowCopy(obj); + expect(copy).toEqual({ a: 1, b: 2 }); + expect(copy).not.toBe(obj); + }); + + it('copies an array', () => { + const arr = [1, 2, 3]; + const copy = shallowCopy(arr); + expect(copy).toEqual([1, 2, 3]); + expect(copy).not.toBe(arr); + }); + + it('should not propagate __proto__ key from source object', () => { + type User = { user: string; admin?: boolean }; + + // @ts-expect-error -- testing prototype pollution + delete Object.prototype.admin; + + // JSON.parse creates an own property named "__proto__" (not the actual prototype) + const malicious = JSON.parse('{"user":"Eve","__proto__":{"admin":true}}'); + + const copy = shallowCopy(malicious); + + // The copy should NOT have admin on its prototype chain + expect((copy as User).admin).toBeUndefined(); + + // Global Object prototype should NOT be polluted + expect(({} as User).admin).toBeUndefined(); + + // @ts-expect-error -- cleanup + delete Object.prototype.admin; + }); + + it('should not propagate constructor key from source object', () => { + type User = { user: string; admin?: boolean }; + + const malicious: User = { + user: 'Eve', + // @ts-expect-error -- intentionally setting constructor to test pollution + constructor: { prototype: { admin: true } }, + }; + + const copy = shallowCopy(malicious); + + expect((copy as User).admin).toBeUndefined(); + + // The constructor of a plain new object should still be Object + expect({}.constructor).toBe(Object); + }); +});
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
10- github.com/immutable-js/immutable-js/security/advisories/GHSA-wf6x-7x77-mvgwnvdExploitVendor AdvisoryWEB
- github.com/advisories/GHSA-wf6x-7x77-mvgwghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-29063ghsaADVISORY
- github.com/immutable-js/immutable-js/commit/16b3313fdf2c5f579f10799e22869f6909abf945ghsaWEB
- github.com/immutable-js/immutable-js/commit/6e2cf1cfe6137e72dfa48fc2cfa8f4d399d113f9ghsaWEB
- github.com/immutable-js/immutable-js/commit/6ed4eb626906df788b08019061b292b90bc718cbghsaWEB
- github.com/immutable-js/immutable-js/issues/2178ghsaWEB
- github.com/immutable-js/immutable-js/releases/tag/v3.8.3nvdRelease NotesWEB
- github.com/immutable-js/immutable-js/releases/tag/v4.3.8nvdRelease NotesWEB
- github.com/immutable-js/immutable-js/releases/tag/v5.1.5nvdRelease NotesWEB
News mentions
11- Zombie linkages are keeping expired domains trusted for yearsHelp Net Security · May 15, 2026
- Red Hat extends open source technology into spaceHelp Net Security · May 11, 2026
- Object First Fleet Manager simplifies distributed backup storageHelp Net Security · May 8, 2026
- Wordfence Intelligence Weekly WordPress Vulnerability Report (April 27, 2026 to May 3, 2026)Wordfence Blog · May 7, 2026
- Why ransomware attacks succeed even when backups existBleepingComputer · May 6, 2026
- Vect 2.0 Ransomware Acts as Wiper, Thanks to Design ErrorDark Reading · Apr 29, 2026
- Lotus Wiper Attack Targets Venezuelan Energy Firms, UtilitiesDark Reading · Apr 29, 2026
- Medical data of 500,000 UK volunteers listed for sale on AlibabaMalwarebytes Labs · Apr 24, 2026
- AI is Changing Vulnerability Discovery and your Software Supply Chain Strategy has to Change with itRapid7 Blog · Apr 23, 2026
- Kyber Ransomware Double Trouble: Windows and ESXi Attacks ExplainedRapid7 Blog · Apr 21, 2026
- Wordfence Intelligence Weekly WordPress Vulnerability Report (April 6, 2026 to April 12, 2026)Wordfence Blog · Apr 16, 2026