Skip to content

Commit 0097398

Browse files
authored
fix: Prototype pollution in Parse.Object and internal APIs; fixes security vulnerability [GHSA-9f2h-7v79-mxw](GHSA-9f2h-7v79-mxw3) (#2749)
1 parent 9e7c1ba commit 0097398

File tree

10 files changed

+625
-35
lines changed

10 files changed

+625
-35
lines changed

src/ObjectStateMutations.ts

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import TaskQueue from './TaskQueue';
66
import { RelationOp } from './ParseOp';
77
import type { Op } from './ParseOp';
88
import type ParseObject from './ParseObject';
9+
import { isDangerousKey } from "./isDangerousKey";
910

1011
export type AttributeMap = Record<string, any>;
1112
export type OpsMap = Record<string, Op>;
@@ -21,17 +22,25 @@ export interface State {
2122

2223
export function defaultState(): State {
2324
return {
24-
serverData: {},
25-
pendingOps: [{}],
26-
objectCache: {},
25+
serverData: Object.create(null),
26+
pendingOps: [Object.create(null)],
27+
objectCache: Object.create(null),
2728
tasks: new TaskQueue(),
2829
existed: false,
2930
};
3031
}
3132

3233
export function setServerData(serverData: AttributeMap, attributes: AttributeMap) {
3334
for (const attr in attributes) {
34-
if (typeof attributes[attr] !== 'undefined') {
35+
// Skip properties from prototype chain
36+
if (!Object.prototype.hasOwnProperty.call(attributes, attr)) {
37+
continue;
38+
}
39+
// Skip dangerous keys that could pollute prototypes
40+
if (isDangerousKey(attr)) {
41+
continue;
42+
}
43+
if (typeof attributes[attr] !== "undefined") {
3544
serverData[attr] = attributes[attr];
3645
} else {
3746
delete serverData[attr];
@@ -40,6 +49,10 @@ export function setServerData(serverData: AttributeMap, attributes: AttributeMap
4049
}
4150

4251
export function setPendingOp(pendingOps: OpsMap[], attr: string, op?: Op) {
52+
// Skip dangerous keys that could pollute prototypes
53+
if (isDangerousKey(attr)) {
54+
return;
55+
}
4356
const last = pendingOps.length - 1;
4457
if (op) {
4558
pendingOps[last][attr] = op;
@@ -49,13 +62,13 @@ export function setPendingOp(pendingOps: OpsMap[], attr: string, op?: Op) {
4962
}
5063

5164
export function pushPendingState(pendingOps: OpsMap[]) {
52-
pendingOps.push({});
65+
pendingOps.push(Object.create(null));
5366
}
5467

5568
export function popPendingState(pendingOps: OpsMap[]): OpsMap {
5669
const first = pendingOps.shift();
5770
if (!pendingOps.length) {
58-
pendingOps[0] = {};
71+
pendingOps[0] = Object.create(null);
5972
}
6073
return first;
6174
}
@@ -64,6 +77,14 @@ export function mergeFirstPendingState(pendingOps: OpsMap[]) {
6477
const first = popPendingState(pendingOps);
6578
const next = pendingOps[0];
6679
for (const attr in first) {
80+
// Skip properties from prototype chain
81+
if (!Object.prototype.hasOwnProperty.call(first, attr)) {
82+
continue;
83+
}
84+
// Skip dangerous keys that could pollute prototypes
85+
if (isDangerousKey(attr)) {
86+
continue;
87+
}
6788
if (next[attr] && first[attr]) {
6889
const merged = next[attr].mergeWith(first[attr]);
6990
if (merged) {
@@ -81,6 +102,10 @@ export function estimateAttribute(
81102
object: ParseObject,
82103
attr: string
83104
): any {
105+
// Skip dangerous keys that could pollute prototypes
106+
if (isDangerousKey(attr)) {
107+
return undefined;
108+
}
84109
let value = serverData[attr];
85110
for (let i = 0; i < pendingOps.length; i++) {
86111
if (pendingOps[i][attr]) {
@@ -101,13 +126,21 @@ export function estimateAttributes(
101126
pendingOps: OpsMap[],
102127
object: ParseObject
103128
): AttributeMap {
104-
const data = {};
129+
const data = Object.create(null);
105130
let attr;
106131
for (attr in serverData) {
107132
data[attr] = serverData[attr];
108133
}
109134
for (let i = 0; i < pendingOps.length; i++) {
110135
for (attr in pendingOps[i]) {
136+
// Skip properties from prototype chain
137+
if (!Object.prototype.hasOwnProperty.call(pendingOps[i], attr)) {
138+
continue;
139+
}
140+
// Skip dangerous keys that could pollute prototypes
141+
if (isDangerousKey(attr)) {
142+
continue;
143+
}
111144
if (pendingOps[i][attr] instanceof RelationOp) {
112145
if (object.id) {
113146
data[attr] = (pendingOps[i][attr] as RelationOp).applyTo(data[attr], object, attr);
@@ -125,7 +158,7 @@ export function estimateAttributes(
125158
if (!isNaN(nextKey)) {
126159
object[key] = [];
127160
} else {
128-
object[key] = {};
161+
object[key] = Object.create(null);
129162
}
130163
} else {
131164
if (Array.isArray(object[key])) {
@@ -165,7 +198,7 @@ function nestedSet(obj, key, value) {
165198
if (!isNaN(nextPath)) {
166199
obj[path] = [];
167200
} else {
168-
obj[path] = {};
201+
obj[path] = Object.create(null);
169202
}
170203
}
171204
obj = obj[path];
@@ -184,6 +217,14 @@ export function commitServerChanges(
184217
) {
185218
const ParseObject = CoreManager.getParseObject();
186219
for (const attr in changes) {
220+
// Skip properties from prototype chain
221+
if (!Object.prototype.hasOwnProperty.call(changes, attr)) {
222+
continue;
223+
}
224+
// Skip dangerous keys that could pollute prototypes
225+
if (isDangerousKey(attr)) {
226+
continue;
227+
}
187228
const val = changes[attr];
188229
nestedSet(serverData, attr, val);
189230
if (

src/ParseObject.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ type ToJSON<T> = {
102102

103103
// Mapping of class names to constructors, so we can populate objects from the
104104
// server with appropriate subclasses of ParseObject
105-
const classMap: AttributeMap = {};
105+
const classMap: AttributeMap = Object.create(null);
106106

107107
// Global counter for generating unique Ids for non-single-instance objects
108108
let objectCount = 0;

src/__tests__/ObjectStateMutations-test.js

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
jest.dontMock('../decode');
22
jest.dontMock('../encode');
33
jest.dontMock('../CoreManager');
4+
jest.dontMock('../isDangerousKey');
45
jest.dontMock('../ObjectStateMutations');
56
jest.dontMock('../ParseFile');
67
jest.dontMock('../ParseGeoPoint');
@@ -11,7 +12,7 @@ jest.dontMock('../TaskQueue');
1112
const mockObject = function (className) {
1213
this.className = className;
1314
};
14-
mockObject.registerSubclass = function () {};
15+
mockObject.registerSubclass = function () { };
1516
jest.setMock('../ParseObject', mockObject);
1617
const CoreManager = require('../CoreManager').default;
1718
CoreManager.setParseObject(mockObject);
@@ -351,4 +352,56 @@ describe('ObjectStateMutations', () => {
351352
existed: false,
352353
});
353354
});
355+
356+
describe('Prototype Pollution Protection', () => {
357+
beforeEach(() => {
358+
// Clear any pollution before each test
359+
delete Object.prototype.polluted;
360+
delete Object.prototype.malicious;
361+
});
362+
363+
afterEach(() => {
364+
// Clean up after tests
365+
delete Object.prototype.polluted;
366+
delete Object.prototype.malicious;
367+
});
368+
369+
it('should not pollute Object.prototype in estimateAttributes with malicious attribute names', () => {
370+
const testObj = {};
371+
372+
const serverData = {};
373+
const pendingOps = [
374+
{
375+
__proto__: new ParseOps.SetOp({ polluted: 'yes' }),
376+
constructor: new ParseOps.SetOp({ malicious: 'data' }),
377+
},
378+
];
379+
380+
ObjectStateMutations.estimateAttributes(serverData, pendingOps, {
381+
className: 'TestClass',
382+
id: 'test123',
383+
});
384+
385+
// Verify Object.prototype was not polluted
386+
expect(testObj.polluted).toBeUndefined();
387+
expect(testObj.malicious).toBeUndefined();
388+
expect({}.polluted).toBeUndefined();
389+
expect({}.malicious).toBeUndefined();
390+
});
391+
392+
it('should not pollute Object.prototype in commitServerChanges with nested __proto__ path', () => {
393+
const testObj = {};
394+
395+
const serverData = {};
396+
const objectCache = {};
397+
ObjectStateMutations.commitServerChanges(serverData, objectCache, {
398+
'__proto__.polluted': 'exploited',
399+
});
400+
401+
// Verify Object.prototype was not polluted
402+
expect(testObj.polluted).toBeUndefined();
403+
expect({}.polluted).toBeUndefined();
404+
expect(Object.prototype.polluted).toBeUndefined();
405+
});
406+
});
354407
});

0 commit comments

Comments
 (0)