Skip to content

Commit 905641c

Browse files
committed
fix: Refactor extend() to handle prototype pollution cases in object merging (#1125)
1 parent 839aa27 commit 905641c

File tree

2 files changed

+271
-0
lines changed

2 files changed

+271
-0
lines changed

src/helpers.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,21 @@ export default function Helpers(mpInstance) {
193193
if ((options = arguments[i]) != null) {
194194
// Extend the base object
195195
for (name in options) {
196+
// Prevent prototype pollution
197+
// https://github.com/advisories/GHSA-jf85-cpcp-j695
198+
if (
199+
name === '__proto__' ||
200+
name === 'constructor' ||
201+
name === 'prototype'
202+
) {
203+
continue;
204+
}
205+
206+
// Only copy own properties
207+
if (!Object.prototype.hasOwnProperty.call(options, name)) {
208+
continue;
209+
}
210+
196211
src = target[name];
197212
copy = options[name];
198213

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import Helpers from '../../src/helpers';
2+
3+
describe('Helpers - Prototype Pollution Protection', () => {
4+
let helpers: any;
5+
let mockMpInstance: any;
6+
7+
beforeEach(() => {
8+
// Clear any potential pollution
9+
delete (Object.prototype as any).isAdmin;
10+
delete (Object.prototype as any).polluted;
11+
delete (Object.prototype as any).testProp;
12+
13+
mockMpInstance = {
14+
_Store: {
15+
SDKConfig: {
16+
flags: {}
17+
}
18+
},
19+
Logger: {
20+
verbose: jest.fn(),
21+
warning: jest.fn(),
22+
error: jest.fn()
23+
}
24+
};
25+
26+
helpers = new Helpers(mockMpInstance);
27+
});
28+
29+
afterEach(() => {
30+
// Cleanup
31+
delete (Object.prototype as any).isAdmin;
32+
delete (Object.prototype as any).polluted;
33+
delete (Object.prototype as any).testProp;
34+
});
35+
36+
describe('extend() - Prototype Pollution Prevention', () => {
37+
it('should block __proto__ in shallow merge', () => {
38+
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
39+
const result = helpers.extend({}, malicious);
40+
41+
expect(typeof result).toBe('object');
42+
const testObj = {};
43+
expect((testObj as any).isAdmin).toBeUndefined();
44+
expect((Object.prototype as any).isAdmin).toBeUndefined();
45+
});
46+
47+
it('should block __proto__ in deep merge', () => {
48+
const malicious = JSON.parse('{"__proto__": {"polluted": "yes"}}');
49+
const result = helpers.extend(true, {}, malicious);
50+
51+
expect(typeof result).toBe('object');
52+
const testObj = {};
53+
expect((testObj as any).polluted).toBeUndefined();
54+
expect((Object.prototype as any).polluted).toBeUndefined();
55+
});
56+
57+
it('should block constructor property', () => {
58+
const malicious = JSON.parse('{"constructor": {"polluted": "constructor"}}');
59+
const result = helpers.extend({}, malicious);
60+
61+
expect(typeof result).toBe('object');
62+
const testObj = {};
63+
expect((testObj as any).polluted).toBeUndefined();
64+
});
65+
66+
it('should block prototype property', () => {
67+
const malicious = JSON.parse('{"prototype": {"polluted": "prototype"}}');
68+
const result = helpers.extend({}, malicious);
69+
70+
expect(typeof result).toBe('object');
71+
const testObj = {};
72+
expect((testObj as any).polluted).toBeUndefined();
73+
});
74+
75+
it('should only copy own properties', () => {
76+
const parent = { inherited: 'value' };
77+
const child = Object.create(parent);
78+
child.own = 'ownValue';
79+
80+
const result = helpers.extend({}, child);
81+
82+
expect(result.own).toBe('ownValue');
83+
expect(result.inherited).toBeUndefined();
84+
});
85+
86+
it('should still merge normal properties correctly', () => {
87+
const source = {
88+
name: 'John',
89+
age: 30,
90+
address: {
91+
city: 'NYC',
92+
zip: '10001'
93+
}
94+
};
95+
96+
const result = helpers.extend(true, {}, source);
97+
98+
expect(result.name).toBe('John');
99+
expect(result.age).toBe(30);
100+
expect(result.address.city).toBe('NYC');
101+
expect(result.address.zip).toBe('10001');
102+
});
103+
104+
it('should handle nested objects without pollution', () => {
105+
const malicious = {
106+
user: {
107+
name: 'John',
108+
__proto__: { isAdmin: true }
109+
}
110+
};
111+
112+
const result = helpers.extend(true, {}, malicious);
113+
114+
expect(result.user.name).toBe('John');
115+
116+
const testObj = {};
117+
expect((testObj as any).isAdmin).toBeUndefined();
118+
expect((Object.prototype as any).isAdmin).toBeUndefined();
119+
});
120+
121+
it('should handle multiple source objects', () => {
122+
const obj1 = { a: 1 };
123+
const obj2 = { b: 2 };
124+
const malicious = JSON.parse('{"__proto__": {"polluted": true}}');
125+
126+
const result = helpers.extend({}, obj1, obj2, malicious);
127+
128+
expect(result.a).toBe(1);
129+
expect(result.b).toBe(2);
130+
131+
const testObj = {};
132+
expect((testObj as any).polluted).toBeUndefined();
133+
});
134+
135+
it('should handle arrays correctly', () => {
136+
const source = {
137+
items: [1, 2, 3],
138+
nested: {
139+
arr: ['a', 'b']
140+
}
141+
};
142+
143+
const result = helpers.extend(true, {}, source);
144+
145+
expect(Array.isArray(result.items)).toBe(true);
146+
expect(result.items).toEqual([1, 2, 3]);
147+
expect(result.nested.arr).toEqual(['a', 'b']);
148+
});
149+
150+
it('should handle objects with null prototype (Object.create(null))', () => {
151+
const nullProtoObj = Object.create(null);
152+
nullProtoObj.name = 'test';
153+
nullProtoObj.value = 42;
154+
155+
const result = helpers.extend({}, nullProtoObj);
156+
157+
expect(result.name).toBe('test');
158+
expect(result.value).toBe(42);
159+
});
160+
161+
it('should handle objects with null prototype in deep merge', () => {
162+
const nullProtoObj = Object.create(null);
163+
nullProtoObj.nested = Object.create(null);
164+
nullProtoObj.nested.deep = 'value';
165+
166+
const result = helpers.extend(true, {}, nullProtoObj);
167+
168+
expect(result.nested.deep).toBe('value');
169+
});
170+
171+
it('should handle null/undefined source arguments gracefully', () => {
172+
const obj1 = { a: 1 };
173+
const obj2 = { b: 2 };
174+
175+
const result = helpers.extend({}, obj1, null, obj2, undefined);
176+
177+
expect(result.a).toBe(1);
178+
expect(result.b).toBe(2);
179+
});
180+
181+
it('should handle null/undefined source arguments in deep merge', () => {
182+
const obj1 = { a: { nested: 1 } };
183+
const obj2 = { b: { nested: 2 } };
184+
185+
const result = helpers.extend(true, {}, obj1, null, obj2, undefined);
186+
187+
expect(result.a.nested).toBe(1);
188+
expect(result.b.nested).toBe(2);
189+
});
190+
191+
it('should handle all null/undefined sources', () => {
192+
const target = { existing: 'value' };
193+
194+
const result = helpers.extend(target, null, undefined, null);
195+
196+
expect(result.existing).toBe('value');
197+
expect(result).toBe(target);
198+
});
199+
});
200+
201+
describe('Real-world attack scenarios', () => {
202+
it('should protect against localStorage-based attack', () => {
203+
// Simulate malicious localStorage data
204+
const localStorageData = JSON.parse('{"__proto__": {"isAdmin": true}, "user": {"name": "attacker"}}');
205+
206+
const result = helpers.extend(false, {}, localStorageData);
207+
208+
expect(result.user.name).toBe('attacker');
209+
210+
const testObj = {};
211+
expect((testObj as any).isAdmin).toBeUndefined();
212+
});
213+
214+
it('should protect against nested pollution attempts', () => {
215+
const malicious = {
216+
config: {
217+
settings: {
218+
__proto__: { polluted: true }
219+
}
220+
}
221+
};
222+
223+
const result = helpers.extend(true, {}, malicious);
224+
225+
expect(typeof result).toBe('object');
226+
const testObj = {};
227+
expect((testObj as any).polluted).toBeUndefined();
228+
});
229+
230+
it('should handle mixed legitimate and malicious data', () => {
231+
const mixed = {
232+
validProp: 'valid',
233+
__proto__: { isAdmin: true },
234+
anotherValid: 123,
235+
constructor: { polluted: true },
236+
nested: {
237+
data: 'ok'
238+
}
239+
};
240+
241+
const result = helpers.extend(true, {}, mixed);
242+
243+
// Valid properties should be copied
244+
expect(result.validProp).toBe('valid');
245+
expect(result.anotherValid).toBe(123);
246+
expect(result.nested.data).toBe('ok');
247+
248+
// Pollution should be blocked
249+
const testObj = {};
250+
expect((testObj as any).isAdmin).toBeUndefined();
251+
expect((testObj as any).polluted).toBeUndefined();
252+
});
253+
});
254+
});
255+
256+

0 commit comments

Comments
 (0)