Skip to content

Commit 6734b7b

Browse files
committed
fix Prototype Pollution in mergeDeep, toJS, etc.
1 parent 6f772de commit 6734b7b

File tree

6 files changed

+56
-0
lines changed

6 files changed

+56
-0
lines changed

__tests__/Map.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,4 +590,13 @@ describe('Map', () => {
590590
])
591591
);
592592
});
593+
594+
it('toJS / toObject are not sensible to prototype pollution', () => {
595+
type User = { user: string; admin?: boolean };
596+
597+
// @ts-expect-error -- intentionally setting __proto__ to test prototype pollution
598+
const m = Map<User>({ user: 'alice' }).set('__proto__', { admin: true });
599+
expect(m.toObject().admin).toBeUndefined();
600+
expect(m.toJS().admin).toBeUndefined();
601+
});
593602
});

__tests__/merge.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,4 +349,33 @@ describe('merge', () => {
349349
// merging with an empty record should return the same empty record instance
350350
expect(merge(myEmptyRecord, { a: 4 })).toBe(myEmptyRecord);
351351
});
352+
353+
it('is not sensible to prototype pollution', () => {
354+
type User = { user: string; admin?: boolean };
355+
356+
// Simulates: app merges HTTP request body (JSON) into user profile
357+
const userProfile: User = { user: 'Alice' };
358+
const requestBody = JSON.parse('{"user":"Eve","__proto__":{"admin":true}}');
359+
360+
const r1 = mergeDeep(userProfile, requestBody);
361+
362+
expect(r1.user).toBe('Eve'); // Eve (updated correctly)
363+
expect(r1.admin).toBeUndefined();
364+
365+
const r2 = mergeDeepWith((a, b) => b, userProfile, requestBody);
366+
expect(r2.admin).toBeUndefined();
367+
368+
const r3 = merge(userProfile, requestBody);
369+
expect(r3.admin).toBeUndefined();
370+
371+
const nested = JSON.parse('{"profile":{"__proto__":{"admin":true}}}');
372+
const r6 = mergeDeep<{ profile: { bio: string; admin?: boolean } }>(
373+
{ profile: { bio: 'Hello' } },
374+
nested
375+
);
376+
377+
expect(r6.profile.admin).toBeUndefined();
378+
379+
expect({}.admin).toBeUndefined(); // Confirm NOT global too
380+
});
352381
});

src/functional/merge.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { isIndexed } from '../predicates/isIndexed';
55
import { isKeyed } from '../predicates/isKeyed';
66
import hasOwnProperty from '../utils/hasOwnProperty';
77
import isDataStructure from '../utils/isDataStructure';
8+
import { isProtoKey } from '../utils/protoInjection';
89
import shallowCopy from '../utils/shallowCopy';
910

1011
export function merge(collection, ...sources) {
@@ -52,6 +53,10 @@ export function mergeWithSources(collection, sources, merger) {
5253
merged.push(value);
5354
}
5455
: (value, key) => {
56+
if (isProtoKey(key)) {
57+
return;
58+
}
59+
5560
const hasVal = hasOwnProperty.call(merged, key);
5661
const nextVal =
5762
hasVal && merger ? merger(merged[key], value, key) : value;

src/methods/toObject.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import assertNotInfinite from '../utils/assertNotInfinite';
2+
import { isProtoKey } from '../utils/protoInjection';
23

34
export function toObject() {
45
assertNotInfinite(this.size);
56
const object = {};
67
this.__iterate((v, k) => {
8+
if (isProtoKey(k)) {
9+
return;
10+
}
11+
712
object[k] = v;
813
});
914
return object;

src/toJS.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Seq } from './Seq';
44
import { isCollection } from './predicates/isCollection';
55
import { isKeyed } from './predicates/isKeyed';
66
import isDataStructure from './utils/isDataStructure';
7+
import { isProtoKey } from './utils/protoInjection';
78

89
export function toJS(
910
value: Collection | Record
@@ -26,6 +27,10 @@ export function toJS(
2627
const result: { [key: string]: unknown } = {};
2728
// @ts-expect-error `__iterate` exists on all Keyed collections but method is not defined in the type
2829
value.__iterate((v, k) => {
30+
if (isProtoKey(k)) {
31+
return;
32+
}
33+
2934
result[k] = toJS(v);
3035
});
3136
return result;

src/utils/protoInjection.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function isProtoKey(key: string): boolean {
2+
return key === '__proto__' || key === 'constructor';
3+
}

0 commit comments

Comments
 (0)