From ee630ffeecbce16862c34bacebe32b6664ba608b Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Fri, 10 Oct 2025 10:28:49 +0200 Subject: [PATCH 1/2] fix: proto pollution single instance state --- src/SingleInstanceStateController.ts | 40 ++++++-- .../SingleInstanceStateController-test.js | 93 ++++++++++++++++++- 2 files changed, 121 insertions(+), 12 deletions(-) diff --git a/src/SingleInstanceStateController.ts b/src/SingleInstanceStateController.ts index 034056b61..f00668e4f 100644 --- a/src/SingleInstanceStateController.ts +++ b/src/SingleInstanceStateController.ts @@ -1,10 +1,17 @@ -import * as ObjectStateMutations from './ObjectStateMutations'; +import * as ObjectStateMutations from "./ObjectStateMutations"; -import type { Op } from './ParseOp'; -import type ParseObject from './ParseObject'; -import type { AttributeMap, ObjectCache, OpsMap, State } from './ObjectStateMutations'; +import type { Op } from "./ParseOp"; +import type ParseObject from "./ParseObject"; +import type { + AttributeMap, + ObjectCache, + OpsMap, + State, +} from "./ObjectStateMutations"; -let objectState: Record> = {}; +// Use Object.create(null) to create an object without prototype chain +// This prevents prototype pollution attacks +let objectState: Record> = Object.create(null); export function getState(obj: ParseObject): State | null { const classData = objectState[obj.className]; @@ -20,7 +27,8 @@ export function initializeState(obj: ParseObject, initial?: State): State { return state; } if (!objectState[obj.className]) { - objectState[obj.className] = {}; + // Use Object.create(null) for nested objects too + objectState[obj.className] = Object.create(null); } if (!initial) { initial = ObjectStateMutations.defaultState(); @@ -90,7 +98,12 @@ export function getObjectCache(obj: ParseObject): ObjectCache { export function estimateAttribute(obj: ParseObject, attr: string): any { const serverData = getServerData(obj); const pendingOps = getPendingOps(obj); - return ObjectStateMutations.estimateAttribute(serverData, pendingOps, obj, attr); + return ObjectStateMutations.estimateAttribute( + serverData, + pendingOps, + obj, + attr + ); } export function estimateAttributes(obj: ParseObject): AttributeMap { @@ -101,16 +114,23 @@ export function estimateAttributes(obj: ParseObject): AttributeMap { export function commitServerChanges(obj: ParseObject, changes: AttributeMap) { const state = initializeState(obj); - ObjectStateMutations.commitServerChanges(state.serverData, state.objectCache, changes); + ObjectStateMutations.commitServerChanges( + state.serverData, + state.objectCache, + changes + ); } -export function enqueueTask(obj: ParseObject, task: () => Promise): Promise { +export function enqueueTask( + obj: ParseObject, + task: () => Promise +): Promise { const state = initializeState(obj); return state.tasks.enqueue(task); } export function clearAllState() { - objectState = {}; + objectState = Object.create(null); } export function duplicateState(source: { id: string }, dest: { id: string }) { diff --git a/src/__tests__/SingleInstanceStateController-test.js b/src/__tests__/SingleInstanceStateController-test.js index 0f7e4c8f4..af35a89b9 100644 --- a/src/__tests__/SingleInstanceStateController-test.js +++ b/src/__tests__/SingleInstanceStateController-test.js @@ -10,8 +10,8 @@ jest.dontMock('../SingleInstanceStateController'); jest.dontMock('../TaskQueue'); jest.dontMock('./test_helpers/flushPromises'); -const mockObject = function () {}; -mockObject.registerSubclass = function () {}; +const mockObject = function () { }; +mockObject.registerSubclass = function () { }; jest.setMock('../ParseObject', { __esModule: true, default: mockObject, @@ -698,4 +698,93 @@ describe('SingleInstanceStateController', () => { existed: false, }); }); + + describe('Prototype Pollution Protection (CVE-2025-57324)', () => { + beforeEach(() => { + SingleInstanceStateController.clearAllState(); + }); + + it('prevents prototype pollution via __proto__ as className', () => { + const testObj = { className: '__proto__', id: 'pollutedProperty' }; + + // Should not throw error (silent prevention via Object.create(null)) + SingleInstanceStateController.initializeState(testObj, {}); + + // Verify no pollution occurred on actual Object.prototype + expect({}.pollutedProperty).toBe(undefined); + expect(Object.prototype.pollutedProperty).toBe(undefined); + }); + + it('prevents prototype pollution via constructor as className', () => { + const testObj = { className: 'constructor', id: 'testId' }; + + // Should not throw error (silent prevention) + SingleInstanceStateController.initializeState(testObj, {}); + + // Verify no pollution occurred + const freshObj = {}; + expect(freshObj.testId).toBe(undefined); + }); + + it('prevents prototype pollution via prototype as className', () => { + const testObj = { className: 'prototype', id: 'testId' }; + + // Should not throw error (silent prevention) + SingleInstanceStateController.initializeState(testObj, {}); + + // Verify no pollution occurred + const freshObj = {}; + expect(freshObj.testId).toBe(undefined); + }); + + it('prevents prototype pollution via __proto__ as id', () => { + const testObj = { className: 'TestClass', id: '__proto__' }; + + // Should not throw error (silent prevention) + SingleInstanceStateController.initializeState(testObj, {}); + + // Verify no pollution occurred + expect({}.TestClass).toBe(undefined); + }); + + it('can store and retrieve data even with dangerous property names', () => { + const testObj1 = { className: '__proto__', id: 'pollutedProperty' }; + const testObj2 = { className: 'constructor', id: 'testId' }; + + // Should work normally without polluting + SingleInstanceStateController.setServerData(testObj1, { value: 'test1' }); + SingleInstanceStateController.setServerData(testObj2, { value: 'test2' }); + + // Should be able to retrieve the data + const state1 = SingleInstanceStateController.getState(testObj1); + const state2 = SingleInstanceStateController.getState(testObj2); + + expect(state1.serverData).toEqual({ value: 'test1' }); + expect(state2.serverData).toEqual({ value: 'test2' }); + + // But no pollution should occur + expect({}.pollutedProperty).toBe(undefined); + expect({}.testId).toBe(undefined); + }); + + it('allows normal className and id values', () => { + const testObj = { className: 'NormalClass', id: 'normalId123' }; + + SingleInstanceStateController.setServerData(testObj, { counter: 12 }); + + const state = SingleInstanceStateController.getState(testObj); + expect(state).toBeTruthy(); + expect(state.serverData).toEqual({ counter: 12 }); + }); + + it('prevents pollution when removing dangerous property names', () => { + const testObj = { className: '__proto__', id: 'dangerousId' }; + + SingleInstanceStateController.setServerData(testObj, { data: 'test' }); + SingleInstanceStateController.removeState(testObj); + + // Verify no pollution occurred + expect({}.dangerousId).toBe(undefined); + }); + }); }); From bb5322587020c0488f6b21e71cb9d26e16141a82 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 14 Oct 2025 19:19:28 +0200 Subject: [PATCH 2/2] Update SingleInstanceStateController.d.ts --- types/SingleInstanceStateController.d.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/types/SingleInstanceStateController.d.ts b/types/SingleInstanceStateController.d.ts index 36bba09ea..8e1d8d6ab 100644 --- a/types/SingleInstanceStateController.d.ts +++ b/types/SingleInstanceStateController.d.ts @@ -1,6 +1,6 @@ -import type { Op } from './ParseOp'; -import type ParseObject from './ParseObject'; -import type { AttributeMap, ObjectCache, OpsMap, State } from './ObjectStateMutations'; +import type { Op } from "./ParseOp"; +import type ParseObject from "./ParseObject"; +import type { AttributeMap, ObjectCache, OpsMap, State } from "./ObjectStateMutations"; export declare function getState(obj: ParseObject): State | null; export declare function initializeState(obj: ParseObject, initial?: State): State; export declare function removeState(obj: ParseObject): State | null;