Skip to content

Commit 9e7c1ba

Browse files
authored
fix: Prototype Pollution vulnerability in SingleInstanceStateController; fixes security vulnerability [GHSA-9g8m-v378-pcg3](GHSA-9g8m-v378-pcg3) (#2745)
1 parent 4a47f65 commit 9e7c1ba

File tree

3 files changed

+124
-15
lines changed

3 files changed

+124
-15
lines changed

src/SingleInstanceStateController.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1-
import * as ObjectStateMutations from './ObjectStateMutations';
1+
import * as ObjectStateMutations from "./ObjectStateMutations";
22

3-
import type { Op } from './ParseOp';
4-
import type ParseObject from './ParseObject';
5-
import type { AttributeMap, ObjectCache, OpsMap, State } from './ObjectStateMutations';
3+
import type { Op } from "./ParseOp";
4+
import type ParseObject from "./ParseObject";
5+
import type {
6+
AttributeMap,
7+
ObjectCache,
8+
OpsMap,
9+
State,
10+
} from "./ObjectStateMutations";
611

7-
let objectState: Record<string, Record<string, State>> = {};
12+
// Use Object.create(null) to create an object without prototype chain
13+
// This prevents prototype pollution attacks
14+
let objectState: Record<string, Record<string, State>> = Object.create(null);
815

916
export function getState(obj: ParseObject): State | null {
1017
const classData = objectState[obj.className];
@@ -20,7 +27,8 @@ export function initializeState(obj: ParseObject, initial?: State): State {
2027
return state;
2128
}
2229
if (!objectState[obj.className]) {
23-
objectState[obj.className] = {};
30+
// Use Object.create(null) for nested objects too
31+
objectState[obj.className] = Object.create(null);
2432
}
2533
if (!initial) {
2634
initial = ObjectStateMutations.defaultState();
@@ -90,7 +98,12 @@ export function getObjectCache(obj: ParseObject): ObjectCache {
9098
export function estimateAttribute(obj: ParseObject, attr: string): any {
9199
const serverData = getServerData(obj);
92100
const pendingOps = getPendingOps(obj);
93-
return ObjectStateMutations.estimateAttribute(serverData, pendingOps, obj, attr);
101+
return ObjectStateMutations.estimateAttribute(
102+
serverData,
103+
pendingOps,
104+
obj,
105+
attr
106+
);
94107
}
95108

96109
export function estimateAttributes(obj: ParseObject): AttributeMap {
@@ -101,16 +114,23 @@ export function estimateAttributes(obj: ParseObject): AttributeMap {
101114

102115
export function commitServerChanges(obj: ParseObject, changes: AttributeMap) {
103116
const state = initializeState(obj);
104-
ObjectStateMutations.commitServerChanges(state.serverData, state.objectCache, changes);
117+
ObjectStateMutations.commitServerChanges(
118+
state.serverData,
119+
state.objectCache,
120+
changes
121+
);
105122
}
106123

107-
export function enqueueTask(obj: ParseObject, task: () => Promise<any>): Promise<void> {
124+
export function enqueueTask(
125+
obj: ParseObject,
126+
task: () => Promise<any>
127+
): Promise<void> {
108128
const state = initializeState(obj);
109129
return state.tasks.enqueue(task);
110130
}
111131

112132
export function clearAllState() {
113-
objectState = {};
133+
objectState = Object.create(null);
114134
}
115135

116136
export function duplicateState(source: { id: string }, dest: { id: string }) {

src/__tests__/SingleInstanceStateController-test.js

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ jest.dontMock('../SingleInstanceStateController');
1010
jest.dontMock('../TaskQueue');
1111
jest.dontMock('./test_helpers/flushPromises');
1212

13-
const mockObject = function () {};
14-
mockObject.registerSubclass = function () {};
13+
const mockObject = function () { };
14+
mockObject.registerSubclass = function () { };
1515
jest.setMock('../ParseObject', {
1616
__esModule: true,
1717
default: mockObject,
@@ -698,4 +698,93 @@ describe('SingleInstanceStateController', () => {
698698
existed: false,
699699
});
700700
});
701+
702+
describe('Prototype Pollution Protection (CVE-2025-57324)', () => {
703+
beforeEach(() => {
704+
SingleInstanceStateController.clearAllState();
705+
});
706+
707+
it('prevents prototype pollution via __proto__ as className', () => {
708+
const testObj = { className: '__proto__', id: 'pollutedProperty' };
709+
710+
// Should not throw error (silent prevention via Object.create(null))
711+
SingleInstanceStateController.initializeState(testObj, {});
712+
713+
// Verify no pollution occurred on actual Object.prototype
714+
expect({}.pollutedProperty).toBe(undefined);
715+
expect(Object.prototype.pollutedProperty).toBe(undefined);
716+
});
717+
718+
it('prevents prototype pollution via constructor as className', () => {
719+
const testObj = { className: 'constructor', id: 'testId' };
720+
721+
// Should not throw error (silent prevention)
722+
SingleInstanceStateController.initializeState(testObj, {});
723+
724+
// Verify no pollution occurred
725+
const freshObj = {};
726+
expect(freshObj.testId).toBe(undefined);
727+
});
728+
729+
it('prevents prototype pollution via prototype as className', () => {
730+
const testObj = { className: 'prototype', id: 'testId' };
731+
732+
// Should not throw error (silent prevention)
733+
SingleInstanceStateController.initializeState(testObj, {});
734+
735+
// Verify no pollution occurred
736+
const freshObj = {};
737+
expect(freshObj.testId).toBe(undefined);
738+
});
739+
740+
it('prevents prototype pollution via __proto__ as id', () => {
741+
const testObj = { className: 'TestClass', id: '__proto__' };
742+
743+
// Should not throw error (silent prevention)
744+
SingleInstanceStateController.initializeState(testObj, {});
745+
746+
// Verify no pollution occurred
747+
expect({}.TestClass).toBe(undefined);
748+
});
749+
750+
it('can store and retrieve data even with dangerous property names', () => {
751+
const testObj1 = { className: '__proto__', id: 'pollutedProperty' };
752+
const testObj2 = { className: 'constructor', id: 'testId' };
753+
754+
// Should work normally without polluting
755+
SingleInstanceStateController.setServerData(testObj1, { value: 'test1' });
756+
SingleInstanceStateController.setServerData(testObj2, { value: 'test2' });
757+
758+
// Should be able to retrieve the data
759+
const state1 = SingleInstanceStateController.getState(testObj1);
760+
const state2 = SingleInstanceStateController.getState(testObj2);
761+
762+
expect(state1.serverData).toEqual({ value: 'test1' });
763+
expect(state2.serverData).toEqual({ value: 'test2' });
764+
765+
// But no pollution should occur
766+
expect({}.pollutedProperty).toBe(undefined);
767+
expect({}.testId).toBe(undefined);
768+
});
769+
770+
it('allows normal className and id values', () => {
771+
const testObj = { className: 'NormalClass', id: 'normalId123' };
772+
773+
SingleInstanceStateController.setServerData(testObj, { counter: 12 });
774+
775+
const state = SingleInstanceStateController.getState(testObj);
776+
expect(state).toBeTruthy();
777+
expect(state.serverData).toEqual({ counter: 12 });
778+
});
779+
780+
it('prevents pollution when removing dangerous property names', () => {
781+
const testObj = { className: '__proto__', id: 'dangerousId' };
782+
783+
SingleInstanceStateController.setServerData(testObj, { data: 'test' });
784+
SingleInstanceStateController.removeState(testObj);
785+
786+
// Verify no pollution occurred
787+
expect({}.dangerousId).toBe(undefined);
788+
});
789+
});
701790
});

types/SingleInstanceStateController.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import type { Op } from './ParseOp';
2-
import type ParseObject from './ParseObject';
3-
import type { AttributeMap, ObjectCache, OpsMap, State } from './ObjectStateMutations';
1+
import type { Op } from "./ParseOp";
2+
import type ParseObject from "./ParseObject";
3+
import type { AttributeMap, ObjectCache, OpsMap, State } from "./ObjectStateMutations";
44
export declare function getState(obj: ParseObject): State | null;
55
export declare function initializeState(obj: ParseObject, initial?: State): State;
66
export declare function removeState(obj: ParseObject): State | null;

0 commit comments

Comments
 (0)