X Tutup
Skip to content

Commit 94bcd3c

Browse files
committed
fix new proto key injection
1 parent faeb58b commit 94bcd3c

File tree

9 files changed

+237
-3
lines changed

9 files changed

+237
-3
lines changed

__tests__/functional/set.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { describe, expect, it } from '@jest/globals';
2+
import { set } from 'immutable';
3+
4+
describe('set', () => {
5+
it('for immutable structure', () => {
6+
const originalArray = ['dog', 'frog', 'cat'];
7+
expect(set(originalArray, 1, 'cow')).toEqual(['dog', 'cow', 'cat']);
8+
expect(set(originalArray, 4, 'cow')).toEqual([
9+
'dog',
10+
'frog',
11+
'cat',
12+
undefined,
13+
'cow',
14+
]);
15+
expect(originalArray).toEqual(['dog', 'frog', 'cat']);
16+
17+
const originalObject = { x: 123, y: 456 };
18+
expect(set(originalObject, 'x', 789)).toEqual({ x: 789, y: 456 });
19+
expect(set(originalObject, 'z', 789)).toEqual({ x: 123, y: 456, z: 789 });
20+
expect(originalObject).toEqual({ x: 123, y: 456 });
21+
});
22+
23+
it('for Array', () => {
24+
const originalArray = ['dog', 'frog', 'cat'];
25+
expect(set(originalArray, 1, 'cow')).toEqual(['dog', 'cow', 'cat']);
26+
expect(set(originalArray, 4, 'cow')).toEqual([
27+
'dog',
28+
'frog',
29+
'cat',
30+
undefined,
31+
'cow',
32+
]);
33+
expect(originalArray).toEqual(['dog', 'frog', 'cat']);
34+
});
35+
36+
it('for plain objects', () => {
37+
const originalObject = { x: 123, y: 456 };
38+
expect(set(originalObject, 'x', 789)).toEqual({ x: 789, y: 456 });
39+
expect(set(originalObject, 'z', 789)).toEqual({ x: 123, y: 456, z: 789 });
40+
expect(originalObject).toEqual({ x: 123, y: 456 });
41+
});
42+
43+
it('is not sensible to prototype pollution via set on plain object', () => {
44+
type User = { user: string; admin?: boolean };
45+
46+
const obj: User = { user: 'Alice' };
47+
// Setting __proto__ key should not change the returned object's prototype chain
48+
// @ts-expect-error -- intentionally setting __proto__ to test prototype pollution
49+
const result = set(obj, '__proto__', { admin: true });
50+
51+
// The returned copy should NOT have 'admin' accessible via prototype
52+
// @ts-expect-error -- testing prototype pollution
53+
expect(result.admin).toBeUndefined();
54+
});
55+
56+
it('is not sensible to prototype pollution via set with JSON.parse source', () => {
57+
type User = { user: string; admin?: boolean };
58+
59+
// JSON.parse creates __proto__ as an own property
60+
const malicious = JSON.parse(
61+
'{"user":"Eve","__proto__":{"admin":true}}'
62+
) as User;
63+
// set on an object that already carries __proto__ from JSON.parse
64+
const result = set(malicious, 'user', 'Alice');
65+
66+
// The returned copy should NOT have 'admin' accessible via prototype pollution
67+
expect(result.admin).toBeUndefined();
68+
});
69+
});

__tests__/functional/update.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { describe, expect, it } from '@jest/globals';
2+
import { update } from 'immutable';
3+
4+
describe('update', () => {
5+
it('for immutable structure', () => {
6+
const originalArray = ['dog', 'frog', 'cat'];
7+
expect(update(originalArray, 1, val => val?.toUpperCase())).toEqual([
8+
'dog',
9+
'FROG',
10+
'cat',
11+
]);
12+
expect(originalArray).toEqual(['dog', 'frog', 'cat']);
13+
14+
const originalObject = { x: 123, y: 456 };
15+
expect(update(originalObject, 'x', val => val * 6)).toEqual({
16+
x: 738,
17+
y: 456,
18+
});
19+
expect(originalObject).toEqual({ x: 123, y: 456 });
20+
});
21+
22+
it('for Array', () => {
23+
const originalArray = ['dog', 'frog', 'cat'];
24+
expect(update(originalArray, 1, val => val?.toUpperCase())).toEqual([
25+
'dog',
26+
'FROG',
27+
'cat',
28+
]);
29+
expect(originalArray).toEqual(['dog', 'frog', 'cat']);
30+
});
31+
32+
it('for plain objects', () => {
33+
const originalObject = { x: 123, y: 456 };
34+
expect(update(originalObject, 'x', val => val * 6)).toEqual({
35+
x: 738,
36+
y: 456,
37+
});
38+
expect(originalObject).toEqual({ x: 123, y: 456 });
39+
});
40+
41+
it('is not sensible to prototype pollution via update on plain object', () => {
42+
type User = { user: string; admin?: boolean };
43+
44+
const obj: User = { user: 'Alice' };
45+
// @ts-expect-error -- intentionally setting __proto__ to test prototype pollution
46+
const result = update(obj, '__proto__', () => ({
47+
admin: true,
48+
})) as unknown as User;
49+
50+
// The returned copy should NOT have 'admin' accessible via prototype
51+
expect(result.admin).toBeUndefined();
52+
});
53+
54+
it('is not sensible to prototype pollution via update with JSON.parse source', () => {
55+
type User = { user: string; admin?: boolean };
56+
57+
// JSON.parse creates __proto__ as an own property
58+
const malicious = JSON.parse('{"user":"Eve","__proto__":{"admin":true}}');
59+
const result = update(malicious, 'user', () => 'Alice') as User;
60+
61+
// The returned copy (via shallowCopy) should NOT have 'admin' via prototype
62+
expect(result.admin).toBeUndefined();
63+
});
64+
});

__tests__/merge.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,7 @@ describe('merge', () => {
357357

358358
expect(r6.profile.admin).toBeUndefined();
359359

360+
// @ts-expect-error -- testing prototype pollution
360361
expect({}.admin).toBeUndefined(); // Confirm NOT global too
361362
});
362363
});

__tests__/updateIn.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,4 +368,36 @@ describe('updateIn', () => {
368368
);
369369
});
370370
});
371+
372+
describe('prototype pollution', () => {
373+
it('setIn on plain object with __proto__ key should not pollute returned object', () => {
374+
type User = { profile: { bio: string }; admin?: boolean };
375+
376+
const obj: User = { profile: { bio: 'Hello' } };
377+
const result = setIn(obj, ['__proto__', 'admin'], true);
378+
379+
// The returned object should NOT have 'admin' accessible via prototype
380+
expect(result.admin).toBeUndefined();
381+
});
382+
383+
it('setIn on plain object with nested __proto__ key should not pollute returned object', () => {
384+
type User = { profile: { bio: string; admin?: boolean } };
385+
386+
const obj: User = { profile: { bio: 'Hello' } };
387+
const result = setIn(obj, ['profile', '__proto__', 'admin'], true);
388+
389+
// The nested object should NOT have 'admin' accessible via prototype
390+
expect(result.profile.admin).toBeUndefined();
391+
});
392+
393+
it('updateIn on plain object with __proto__ key should not pollute returned object', () => {
394+
type User = { profile: { bio: string }; admin?: boolean };
395+
396+
const obj: User = { profile: { bio: 'Hello' } };
397+
const result = updateIn(obj, ['__proto__', 'admin'], () => true);
398+
399+
// The returned object should NOT have 'admin' accessible via prototype
400+
expect(result.admin).toBeUndefined();
401+
});
402+
});
371403
});

__tests__/utils/shallowCopy.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { describe, it, expect } from '@jest/globals';
2+
import shallowCopy from '../../src/utils/shallowCopy';
3+
4+
describe('shallowCopy', () => {
5+
it('copies a plain object', () => {
6+
const obj = { a: 1, b: 2 };
7+
const copy = shallowCopy(obj);
8+
expect(copy).toEqual({ a: 1, b: 2 });
9+
expect(copy).not.toBe(obj);
10+
});
11+
12+
it('copies an array', () => {
13+
const arr = [1, 2, 3];
14+
const copy = shallowCopy(arr);
15+
expect(copy).toEqual([1, 2, 3]);
16+
expect(copy).not.toBe(arr);
17+
});
18+
19+
it('should not propagate __proto__ key from source object', () => {
20+
type User = { user: string; admin?: boolean };
21+
22+
// @ts-expect-error -- testing prototype pollution
23+
delete Object.prototype.admin;
24+
25+
// JSON.parse creates an own property named "__proto__" (not the actual prototype)
26+
const malicious = JSON.parse('{"user":"Eve","__proto__":{"admin":true}}');
27+
28+
const copy = shallowCopy(malicious);
29+
30+
// The copy should NOT have admin on its prototype chain
31+
expect((copy as User).admin).toBeUndefined();
32+
33+
// Global Object prototype should NOT be polluted
34+
expect(({} as User).admin).toBeUndefined();
35+
36+
// @ts-expect-error -- cleanup
37+
delete Object.prototype.admin;
38+
});
39+
40+
it('should not propagate constructor key from source object', () => {
41+
type User = { user: string; admin?: boolean };
42+
43+
const malicious: User = {
44+
user: 'Eve',
45+
// @ts-expect-error -- intentionally setting constructor to test pollution
46+
constructor: { prototype: { admin: true } },
47+
};
48+
49+
const copy = shallowCopy(malicious);
50+
51+
expect((copy as User).admin).toBeUndefined();
52+
53+
// The constructor of a plain new object should still be Object
54+
expect({}.constructor).toBe(Object);
55+
});
56+
});

src/functional/set.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import { isImmutable } from '../predicates/isImmutable';
22
import hasOwnProperty from '../utils/hasOwnProperty';
33
import isDataStructure from '../utils/isDataStructure';
4+
import { isProtoKey } from '../utils/protoInjection';
45
import shallowCopy from '../utils/shallowCopy';
56

67
export function set(collection, key, value) {
8+
if (isProtoKey(key)) {
9+
return collection;
10+
}
11+
712
if (!isDataStructure(collection)) {
813
throw new TypeError(
914
'Cannot update non-data-structure value: ' + collection

src/utils/protoInjection.js

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

src/utils/protoInjection.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/utils/shallowCopy.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import arrCopy from './arrCopy';
22
import hasOwnProperty from './hasOwnProperty';
3+
import { isProtoKey } from './protoInjection';
34

45
export default function shallowCopy(from) {
56
if (Array.isArray(from)) {
67
return arrCopy(from);
78
}
89
const to = {};
910
for (const key in from) {
11+
if (isProtoKey(key)) {
12+
continue;
13+
}
14+
1015
if (hasOwnProperty.call(from, key)) {
1116
to[key] = from[key];
1217
}

0 commit comments

Comments
 (0)
X Tutup