CVE-2025-62374
Description
Parse Javascript SDK provides access to the powerful Parse Server backend from your JavaScript app. Prior to 7.0.0, injection of malicious payload allows attacker to remotely execute arbitrary code. ParseObject.fromJSON, ParseObject.pin, ParseObject.registerSubclass, ObjectStateMutations (internal), and encode/decode (internal) are affected. This vulnerability is fixed in 7.0.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
parsenpm | < 7.0.0 | 7.0.0 |
Affected products
1- Range: 2.1.0, 2.10.0, 2.11.0, …
Patches
100973987f361fix: Prototype pollution in `Parse.Object` and internal APIs; fixes security vulnerability [GHSA-9f2h-7v79-mxw](https://github.com/parse-community/Parse-SDK-JS/security/advisories/GHSA-9f2h-7v79-mxw3) (#2749)
10 files changed · +625 −35
src/decode.ts+8 −1 modified@@ -3,6 +3,7 @@ import ParseFile from './ParseFile'; import ParseGeoPoint from './ParseGeoPoint'; import ParsePolygon from './ParsePolygon'; import ParseRelation from './ParseRelation'; +import { isDangerousKey } from "./isDangerousKey"; export default function decode(value: any): any { if (value === null || typeof value !== 'object' || value instanceof Date) { @@ -49,7 +50,13 @@ export default function decode(value: any): any { } const copy = {}; for (const k in value) { - copy[k] = decode(value[k]); + if (Object.prototype.hasOwnProperty.call(value, k)) { + // Skip dangerous keys that could pollute prototypes + if (isDangerousKey(k)) { + continue; + } + copy[k] = decode(value[k]); + } } return copy; }
src/encode.ts+15 −1 modified@@ -4,6 +4,7 @@ import ParseFile from './ParseFile'; import ParseGeoPoint from './ParseGeoPoint'; import ParsePolygon from './ParsePolygon'; import ParseRelation from './ParseRelation'; +import { isDangerousKey } from "./isDangerousKey"; function encode( value: any, @@ -71,7 +72,20 @@ function encode( if (value && typeof value === 'object') { const output = {}; for (const k in value) { - output[k] = encode(value[k], disallowObjects, forcePointers, seen, offline); + // Only iterate over own properties + if (Object.prototype.hasOwnProperty.call(value, k)) { + // Skip dangerous keys that could pollute prototypes + if (isDangerousKey(k)) { + continue; + } + output[k] = encode( + value[k], + disallowObjects, + forcePointers, + seen, + offline + ); + } } return output; }
src/isDangerousKey.ts+19 −0 added@@ -0,0 +1,19 @@ +/** + * Check if a property name or path is potentially dangerous for prototype pollution + * Dangerous keys include: __proto__, constructor, prototype + * @param key - The property name or dotted path to check + * @returns true if the key is dangerous, false otherwise + */ +export function isDangerousKey(key: string): boolean { + const dangerousKeys = ["__proto__", "constructor", "prototype"]; + // Check if the key itself is dangerous + if (dangerousKeys.includes(key)) { + return true; + } + // Check if any part of a dotted path is dangerous + if (key.includes(".")) { + const parts = key.split("."); + return parts.some((part) => dangerousKeys.includes(part)); + } + return false; +}
src/ObjectStateMutations.ts+50 −9 modified@@ -6,6 +6,7 @@ import TaskQueue from './TaskQueue'; import { RelationOp } from './ParseOp'; import type { Op } from './ParseOp'; import type ParseObject from './ParseObject'; +import { isDangerousKey } from "./isDangerousKey"; export type AttributeMap = Record<string, any>; export type OpsMap = Record<string, Op>; @@ -21,17 +22,25 @@ export interface State { export function defaultState(): State { return { - serverData: {}, - pendingOps: [{}], - objectCache: {}, + serverData: Object.create(null), + pendingOps: [Object.create(null)], + objectCache: Object.create(null), tasks: new TaskQueue(), existed: false, }; } export function setServerData(serverData: AttributeMap, attributes: AttributeMap) { for (const attr in attributes) { - if (typeof attributes[attr] !== 'undefined') { + // Skip properties from prototype chain + if (!Object.prototype.hasOwnProperty.call(attributes, attr)) { + continue; + } + // Skip dangerous keys that could pollute prototypes + if (isDangerousKey(attr)) { + continue; + } + if (typeof attributes[attr] !== "undefined") { serverData[attr] = attributes[attr]; } else { delete serverData[attr]; @@ -40,6 +49,10 @@ export function setServerData(serverData: AttributeMap, attributes: AttributeMap } export function setPendingOp(pendingOps: OpsMap[], attr: string, op?: Op) { + // Skip dangerous keys that could pollute prototypes + if (isDangerousKey(attr)) { + return; + } const last = pendingOps.length - 1; if (op) { pendingOps[last][attr] = op; @@ -49,13 +62,13 @@ export function setPendingOp(pendingOps: OpsMap[], attr: string, op?: Op) { } export function pushPendingState(pendingOps: OpsMap[]) { - pendingOps.push({}); + pendingOps.push(Object.create(null)); } export function popPendingState(pendingOps: OpsMap[]): OpsMap { const first = pendingOps.shift(); if (!pendingOps.length) { - pendingOps[0] = {}; + pendingOps[0] = Object.create(null); } return first; } @@ -64,6 +77,14 @@ export function mergeFirstPendingState(pendingOps: OpsMap[]) { const first = popPendingState(pendingOps); const next = pendingOps[0]; for (const attr in first) { + // Skip properties from prototype chain + if (!Object.prototype.hasOwnProperty.call(first, attr)) { + continue; + } + // Skip dangerous keys that could pollute prototypes + if (isDangerousKey(attr)) { + continue; + } if (next[attr] && first[attr]) { const merged = next[attr].mergeWith(first[attr]); if (merged) { @@ -81,6 +102,10 @@ export function estimateAttribute( object: ParseObject, attr: string ): any { + // Skip dangerous keys that could pollute prototypes + if (isDangerousKey(attr)) { + return undefined; + } let value = serverData[attr]; for (let i = 0; i < pendingOps.length; i++) { if (pendingOps[i][attr]) { @@ -101,13 +126,21 @@ export function estimateAttributes( pendingOps: OpsMap[], object: ParseObject ): AttributeMap { - const data = {}; + const data = Object.create(null); let attr; for (attr in serverData) { data[attr] = serverData[attr]; } for (let i = 0; i < pendingOps.length; i++) { for (attr in pendingOps[i]) { + // Skip properties from prototype chain + if (!Object.prototype.hasOwnProperty.call(pendingOps[i], attr)) { + continue; + } + // Skip dangerous keys that could pollute prototypes + if (isDangerousKey(attr)) { + continue; + } if (pendingOps[i][attr] instanceof RelationOp) { if (object.id) { data[attr] = (pendingOps[i][attr] as RelationOp).applyTo(data[attr], object, attr); @@ -125,7 +158,7 @@ export function estimateAttributes( if (!isNaN(nextKey)) { object[key] = []; } else { - object[key] = {}; + object[key] = Object.create(null); } } else { if (Array.isArray(object[key])) { @@ -165,7 +198,7 @@ function nestedSet(obj, key, value) { if (!isNaN(nextPath)) { obj[path] = []; } else { - obj[path] = {}; + obj[path] = Object.create(null); } } obj = obj[path]; @@ -184,6 +217,14 @@ export function commitServerChanges( ) { const ParseObject = CoreManager.getParseObject(); for (const attr in changes) { + // Skip properties from prototype chain + if (!Object.prototype.hasOwnProperty.call(changes, attr)) { + continue; + } + // Skip dangerous keys that could pollute prototypes + if (isDangerousKey(attr)) { + continue; + } const val = changes[attr]; nestedSet(serverData, attr, val); if (
src/ParseObject.ts+1 −1 modified@@ -102,7 +102,7 @@ type ToJSON<T> = { // Mapping of class names to constructors, so we can populate objects from the // server with appropriate subclasses of ParseObject -const classMap: AttributeMap = {}; +const classMap: AttributeMap = Object.create(null); // Global counter for generating unique Ids for non-single-instance objects let objectCount = 0;
src/__tests__/decode-test.js+189 −0 modified@@ -1,5 +1,6 @@ jest.dontMock('../decode'); jest.dontMock('../CoreManager'); +jest.dontMock('../isDangerousKey'); jest.dontMock('../ParseFile'); jest.dontMock('../ParseGeoPoint'); jest.dontMock('../ParseObject'); @@ -120,4 +121,192 @@ describe('decode', () => { count: 15, }); }); + + describe('Prototype Pollution Protection', () => { + beforeEach(() => { + // Clear any pollution before each test + delete Object.prototype.polluted; + delete Object.prototype.malicious; + delete Object.prototype.exploit; + }); + + afterEach(() => { + // Clean up after tests + delete Object.prototype.polluted; + delete Object.prototype.malicious; + delete Object.prototype.exploit; + }); + + it('should not pollute Object.prototype when decoding object with __proto__ key', () => { + const testObj = {}; + const maliciousInput = { + normalKey: 'value', + __proto__: { polluted: 'yes' }, + }; + + const result = decode(maliciousInput); + + // Verify Object.prototype was not polluted + expect(testObj.polluted).toBeUndefined(); + expect({}.polluted).toBeUndefined(); + expect(Object.prototype.polluted).toBeUndefined(); + + // Verify result only has own property + expect(Object.prototype.hasOwnProperty.call(result, '__proto__')).toBe(false); + expect(result.normalKey).toBe('value'); + }); + + it('should not pollute Object.prototype when decoding object with constructor key', () => { + const testObj = {}; + const maliciousInput = { + normalKey: 'value', + constructor: { polluted: 'yes' }, + }; + + const result = decode(maliciousInput); + + // Verify Object.prototype was not polluted + expect(testObj.polluted).toBeUndefined(); + expect({}.polluted).toBeUndefined(); + expect(Object.prototype.polluted).toBeUndefined(); + + // Verify result doesn't contain constructor from prototype chain + expect(result.normalKey).toBe('value'); + }); + + it('should not pollute Object.prototype when decoding object with prototype key', () => { + const testObj = {}; + const maliciousInput = { + normalKey: 'value', + prototype: { polluted: 'yes' }, + }; + + const result = decode(maliciousInput); + + // Verify Object.prototype was not polluted + expect(testObj.polluted).toBeUndefined(); + expect({}.polluted).toBeUndefined(); + expect(Object.prototype.polluted).toBeUndefined(); + + // Verify result contains only own properties + expect(result.normalKey).toBe('value'); + }); + + it('should not pollute Object.prototype when decoding nested objects with dangerous keys', () => { + const testObj = {}; + const maliciousInput = { + nested: { + __proto__: { polluted: 'nested' }, + data: 'value', + }, + normal: 'key', + }; + + const result = decode(maliciousInput); + + // Verify Object.prototype was not polluted + expect(testObj.polluted).toBeUndefined(); + expect({}.polluted).toBeUndefined(); + expect(Object.prototype.polluted).toBeUndefined(); + + // Verify result structure + expect(result.normal).toBe('key'); + expect(result.nested).toBeDefined(); + expect(result.nested.data).toBe('value'); + expect(Object.prototype.hasOwnProperty.call(result.nested, '__proto__')).toBe(false); + }); + + it('should not pollute Object.prototype when decoding arrays with objects containing dangerous keys', () => { + const testObj = {}; + const maliciousInput = [ + { __proto__: { polluted: 'array1' } }, + { constructor: { malicious: 'array2' } }, + { normalKey: 'value' }, + ]; + + const result = decode(maliciousInput); + + // Verify Object.prototype was not polluted + expect(testObj.polluted).toBeUndefined(); + expect(testObj.malicious).toBeUndefined(); + expect({}.polluted).toBeUndefined(); + expect({}.malicious).toBeUndefined(); + expect(Object.prototype.polluted).toBeUndefined(); + expect(Object.prototype.malicious).toBeUndefined(); + + // Verify result array + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(3); + expect(result[2].normalKey).toBe('value'); + }); + + it('should only decode own properties, not inherited ones', () => { + const parent = { inherited: 'parent' }; + const child = Object.create(parent); + child.own = 'child'; + + const result = decode(child); + + // Should only include own property + expect(result.own).toBe('child'); + expect(result.inherited).toBeUndefined(); + }); + + it('should not decode properties from prototype chain', () => { + Object.prototype.exploit = 'malicious'; + const obj = { normalKey: 'value' }; + + const result = decode(obj); + + // Should not include prototype property + expect(result.normalKey).toBe('value'); + expect(Object.prototype.hasOwnProperty.call(result, 'exploit')).toBe(false); + + delete Object.prototype.exploit; + }); + + it('should not pollute Object.prototype when decoding Parse type with dangerous className', () => { + const testObj = {}; + const maliciousInput = { + __type: 'Pointer', + className: '__proto__', + objectId: 'test123', + }; + + // This should be handled by ParseObject.fromJSON + decode(maliciousInput); + + // Verify Object.prototype was not polluted + expect(testObj.polluted).toBeUndefined(); + expect({}.polluted).toBeUndefined(); + expect(Object.prototype.polluted).toBeUndefined(); + }); + + it('should not pollute Object.prototype when decoding deeply nested dangerous keys', () => { + const testObj = {}; + const maliciousInput = { + level1: { + level2: { + level3: { + __proto__: { polluted: 'deep' }, + normalData: 'value', + }, + }, + }, + }; + + const result = decode(maliciousInput); + + // Verify Object.prototype was not polluted + expect(testObj.polluted).toBeUndefined(); + expect({}.polluted).toBeUndefined(); + expect(Object.prototype.polluted).toBeUndefined(); + + // Verify result structure is preserved (without dangerous keys) + expect(result.level1.level2.level3.normalData).toBe('value'); + expect(Object.prototype.hasOwnProperty.call(result.level1.level2.level3, '__proto__')).toBe( + false + ); + }); + }); });
src/__tests__/encode-test.js+145 −0 modified@@ -1,4 +1,5 @@ jest.dontMock('../encode'); +jest.dontMock('../isDangerousKey'); jest.dontMock('../ParseACL'); jest.dontMock('../ParseFile'); jest.dontMock('../ParseGeoPoint'); @@ -193,4 +194,148 @@ describe('encode', () => { str: 'abc', }); }); + + describe('Prototype Pollution Protection', () => { + beforeEach(() => { + // Clear any pollution before each test + delete Object.prototype.polluted; + delete Object.prototype.malicious; + delete Object.prototype.exploit; + }); + + afterEach(() => { + // Clean up after tests + delete Object.prototype.polluted; + delete Object.prototype.malicious; + delete Object.prototype.exploit; + }); + + it('should not pollute Object.prototype when encoding object with __proto__ key', () => { + const testObj = {}; + const maliciousInput = { + normalKey: 'value', + __proto__: { polluted: 'yes' }, + }; + + const result = encode(maliciousInput); + + // Verify Object.prototype was not polluted + expect(testObj.polluted).toBeUndefined(); + expect({}.polluted).toBeUndefined(); + expect(Object.prototype.polluted).toBeUndefined(); + + // Verify result only has own property + expect(Object.prototype.hasOwnProperty.call(result, '__proto__')).toBe(false); + expect(result.normalKey).toBe('value'); + }); + + it('should not pollute Object.prototype when encoding object with constructor key', () => { + const testObj = {}; + const maliciousInput = { + normalKey: 'value', + constructor: { polluted: 'yes' }, + }; + + const result = encode(maliciousInput); + + // Verify Object.prototype was not polluted + expect(testObj.polluted).toBeUndefined(); + expect({}.polluted).toBeUndefined(); + expect(Object.prototype.polluted).toBeUndefined(); + + // Verify result doesn't contain constructor from prototype chain + expect(result.normalKey).toBe('value'); + }); + + it('should not pollute Object.prototype when encoding object with prototype key', () => { + const testObj = {}; + const maliciousInput = { + normalKey: 'value', + prototype: { polluted: 'yes' }, + }; + + const result = encode(maliciousInput); + + // Verify Object.prototype was not polluted + expect(testObj.polluted).toBeUndefined(); + expect({}.polluted).toBeUndefined(); + expect(Object.prototype.polluted).toBeUndefined(); + + // Verify result contains only own properties + expect(result.normalKey).toBe('value'); + }); + + it('should not pollute Object.prototype when encoding nested objects with dangerous keys', () => { + const testObj = {}; + const maliciousInput = { + nested: { + __proto__: { polluted: 'nested' }, + data: 'value', + }, + normal: 'key', + }; + + const result = encode(maliciousInput); + + // Verify Object.prototype was not polluted + expect(testObj.polluted).toBeUndefined(); + expect({}.polluted).toBeUndefined(); + expect(Object.prototype.polluted).toBeUndefined(); + + // Verify result structure + expect(result.normal).toBe('key'); + expect(result.nested).toBeDefined(); + expect(result.nested.data).toBe('value'); + expect(Object.prototype.hasOwnProperty.call(result.nested, '__proto__')).toBe(false); + }); + + it('should not pollute Object.prototype when encoding arrays with objects containing dangerous keys', () => { + const testObj = {}; + const maliciousInput = [ + { __proto__: { polluted: 'array1' } }, + { constructor: { malicious: 'array2' } }, + { normalKey: 'value' }, + ]; + + const result = encode(maliciousInput); + + // Verify Object.prototype was not polluted + expect(testObj.polluted).toBeUndefined(); + expect(testObj.malicious).toBeUndefined(); + expect({}.polluted).toBeUndefined(); + expect({}.malicious).toBeUndefined(); + expect(Object.prototype.polluted).toBeUndefined(); + expect(Object.prototype.malicious).toBeUndefined(); + + // Verify result array + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(3); + expect(result[2].normalKey).toBe('value'); + }); + + it('should only encode own properties, not inherited ones', () => { + const parent = { inherited: 'parent' }; + const child = Object.create(parent); + child.own = 'child'; + + const result = encode(child); + + // Should only include own property + expect(result.own).toBe('child'); + expect(result.inherited).toBeUndefined(); + }); + + it('should not encode properties from prototype chain', () => { + Object.prototype.exploit = 'malicious'; + const obj = { normalKey: 'value' }; + + const result = encode(obj); + + // Should not include prototype property + expect(result.normalKey).toBe('value'); + expect(Object.prototype.hasOwnProperty.call(result, 'exploit')).toBe(false); + + delete Object.prototype.exploit; + }); + }); });
src/__tests__/ObjectStateMutations-test.js+54 −1 modified@@ -1,6 +1,7 @@ jest.dontMock('../decode'); jest.dontMock('../encode'); jest.dontMock('../CoreManager'); +jest.dontMock('../isDangerousKey'); jest.dontMock('../ObjectStateMutations'); jest.dontMock('../ParseFile'); jest.dontMock('../ParseGeoPoint'); @@ -11,7 +12,7 @@ jest.dontMock('../TaskQueue'); const mockObject = function (className) { this.className = className; }; -mockObject.registerSubclass = function () {}; +mockObject.registerSubclass = function () { }; jest.setMock('../ParseObject', mockObject); const CoreManager = require('../CoreManager').default; CoreManager.setParseObject(mockObject); @@ -351,4 +352,56 @@ describe('ObjectStateMutations', () => { existed: false, }); }); + + describe('Prototype Pollution Protection', () => { + beforeEach(() => { + // Clear any pollution before each test + delete Object.prototype.polluted; + delete Object.prototype.malicious; + }); + + afterEach(() => { + // Clean up after tests + delete Object.prototype.polluted; + delete Object.prototype.malicious; + }); + + it('should not pollute Object.prototype in estimateAttributes with malicious attribute names', () => { + const testObj = {}; + + const serverData = {}; + const pendingOps = [ + { + __proto__: new ParseOps.SetOp({ polluted: 'yes' }), + constructor: new ParseOps.SetOp({ malicious: 'data' }), + }, + ]; + + ObjectStateMutations.estimateAttributes(serverData, pendingOps, { + className: 'TestClass', + id: 'test123', + }); + + // Verify Object.prototype was not polluted + expect(testObj.polluted).toBeUndefined(); + expect(testObj.malicious).toBeUndefined(); + expect({}.polluted).toBeUndefined(); + expect({}.malicious).toBeUndefined(); + }); + + it('should not pollute Object.prototype in commitServerChanges with nested __proto__ path', () => { + const testObj = {}; + + const serverData = {}; + const objectCache = {}; + ObjectStateMutations.commitServerChanges(serverData, objectCache, { + '__proto__.polluted': 'exploited', + }); + + // Verify Object.prototype was not polluted + expect(testObj.polluted).toBeUndefined(); + expect({}.polluted).toBeUndefined(); + expect(Object.prototype.polluted).toBeUndefined(); + }); + }); });
src/__tests__/ParseObject-test.js+137 −22 modified@@ -166,8 +166,8 @@ CoreManager.setInstallationController({ currentInstallationId() { return Promise.resolve('iid'); }, - currentInstallation() {}, - updateInstallationOnDisk() {}, + currentInstallation() { }, + updateInstallationOnDisk() { }, }); CoreManager.set('APPLICATION_ID', 'A'); CoreManager.set('JAVASCRIPT_KEY', 'B'); @@ -1413,9 +1413,9 @@ describe('ParseObject', () => { const objectController = CoreManager.getObjectController(); const spy = jest .spyOn(objectController, 'fetch') - .mockImplementationOnce(() => {}) - .mockImplementationOnce(() => {}) - .mockImplementationOnce(() => {}); + .mockImplementationOnce(() => { }) + .mockImplementationOnce(() => { }) + .mockImplementationOnce(() => { }); const parent = new ParseObject('Person'); await parent.fetchWithInclude('child', { @@ -1517,9 +1517,9 @@ describe('ParseObject', () => { const objectController = CoreManager.getObjectController(); const spy = jest .spyOn(objectController, 'fetch') - .mockImplementationOnce(() => {}) - .mockImplementationOnce(() => {}) - .mockImplementationOnce(() => {}); + .mockImplementationOnce(() => { }) + .mockImplementationOnce(() => { }) + .mockImplementationOnce(() => { }); const parent = new ParseObject('Person'); await ParseObject.fetchAllWithInclude([parent], 'child', { @@ -1545,9 +1545,9 @@ describe('ParseObject', () => { const objectController = CoreManager.getObjectController(); const spy = jest .spyOn(objectController, 'fetch') - .mockImplementationOnce(() => {}) - .mockImplementationOnce(() => {}) - .mockImplementationOnce(() => {}); + .mockImplementationOnce(() => { }) + .mockImplementationOnce(() => { }) + .mockImplementationOnce(() => { }); const parent = new ParseObject('Person'); await ParseObject.fetchAllIfNeededWithInclude([parent], 'child', { @@ -1610,7 +1610,7 @@ describe('ParseObject', () => { }); it('can save the object eventually', async () => { - mockFetch([{ status: 200, response: {objectId: 'PFEventually' } }]); + mockFetch([{ status: 200, response: { objectId: 'PFEventually' } }]); const p = new ParseObject('Person'); p.set('age', 38); const obj = await p.saveEventually(); @@ -1623,7 +1623,7 @@ describe('ParseObject', () => { it('can save the object eventually on network failure', async () => { const p = new ParseObject('Person'); jest.spyOn(EventuallyQueue, 'save').mockImplementationOnce(() => Promise.resolve()); - jest.spyOn(EventuallyQueue, 'poll').mockImplementationOnce(() => {}); + jest.spyOn(EventuallyQueue, 'poll').mockImplementationOnce(() => { }); jest.spyOn(p, 'save').mockImplementationOnce(() => { throw new ParseError( ParseError.CONNECTION_FAILED, @@ -1638,7 +1638,7 @@ describe('ParseObject', () => { it('should not save the object eventually on error', async () => { const p = new ParseObject('Person'); jest.spyOn(EventuallyQueue, 'save').mockImplementationOnce(() => Promise.resolve()); - jest.spyOn(EventuallyQueue, 'poll').mockImplementationOnce(() => {}); + jest.spyOn(EventuallyQueue, 'poll').mockImplementationOnce(() => { }); jest.spyOn(p, 'save').mockImplementationOnce(() => { throw new ParseError(ParseError.OTHER_CAUSE, 'Tried to save a batch with a cycle.'); }); @@ -1751,12 +1751,12 @@ describe('ParseObject', () => { const p = new ParseObject('Per$on'); expect(p._getPendingOps().length).toBe(1); p.increment('updates'); - p.save().catch(() => {}); + p.save().catch(() => { }); jest.runAllTicks(); await flushPromises(); expect(p._getPendingOps().length).toBe(1); p.set('updates', 12); - p.save().catch(() => {}); + p.save().catch(() => { }); jest.runAllTicks(); await flushPromises(); expect(p._getPendingOps().length).toBe(1); @@ -2491,7 +2491,7 @@ describe('ObjectController', () => { it('can fetch a single object', async () => { const objectController = CoreManager.getObjectController(); - mockFetch([{ status: 200, response: { objectId: 'pid'} }]); + mockFetch([{ status: 200, response: { objectId: 'pid' } }]); const o = new ParseObject('Person'); o.id = 'pid'; @@ -2535,7 +2535,7 @@ describe('ObjectController', () => { it('can fetch a single object with include', async () => { expect.assertions(2); const objectController = CoreManager.getObjectController(); - mockFetch([{ status: 200, response: { objectId: 'pid'} }]); + mockFetch([{ status: 200, response: { objectId: 'pid' } }]); const o = new ParseObject('Person'); o.id = 'pid'; @@ -2706,7 +2706,7 @@ describe('ObjectController', () => { it('can destroy the object eventually on network failure', async () => { const p = new ParseObject('Person'); jest.spyOn(EventuallyQueue, 'destroy').mockImplementationOnce(() => Promise.resolve()); - jest.spyOn(EventuallyQueue, 'poll').mockImplementationOnce(() => {}); + jest.spyOn(EventuallyQueue, 'poll').mockImplementationOnce(() => { }); jest.spyOn(p, 'destroy').mockImplementationOnce(() => { throw new ParseError( ParseError.CONNECTION_FAILED, @@ -2721,7 +2721,7 @@ describe('ObjectController', () => { it('should not destroy object eventually on error', async () => { const p = new ParseObject('Person'); jest.spyOn(EventuallyQueue, 'destroy').mockImplementationOnce(() => Promise.resolve()); - jest.spyOn(EventuallyQueue, 'poll').mockImplementationOnce(() => {}); + jest.spyOn(EventuallyQueue, 'poll').mockImplementationOnce(() => { }); jest.spyOn(p, 'destroy').mockImplementationOnce(() => { throw new ParseError(ParseError.OTHER_CAUSE, 'Unable to delete.'); }); @@ -2758,7 +2758,7 @@ describe('ObjectController', () => { for (let i = 0; i < 3; i++) { responses.push({ status: 200, - response:{ + response: { name: names[i], url: 'http://files.parsetfss.com/a/' + names[i], }, @@ -3117,7 +3117,7 @@ describe('ParseObject Subclasses', () => { }); it('can use on ParseObject subclass for multiple Parse.Object class names', () => { - class MyParseObjects extends ParseObject {} + class MyParseObjects extends ParseObject { } ParseObject.registerSubclass('TestObject', MyParseObjects); ParseObject.registerSubclass('TestObject1', MyParseObjects); ParseObject.registerSubclass('TestObject2', MyParseObjects); @@ -3542,4 +3542,119 @@ describe('ParseObject pin', () => { ); CoreManager.set('NODE_LOGGING', false); }); + + describe('Prototype Pollution Protection', () => { + beforeEach(() => { + // Clear any pollution before each test + delete Object.prototype.polluted; + delete Object.prototype.malicious; + delete Object.prototype.exploit; + }); + + afterEach(() => { + // Clean up after tests + delete Object.prototype.polluted; + delete Object.prototype.malicious; + delete Object.prototype.exploit; + }); + + it('should not pollute Object.prototype via prototype className in registerSubclass', () => { + const testObj = {}; + + class MaliciousClass extends ParseObject { + constructor() { + super('prototype'); + } + } + + ParseObject.registerSubclass('prototype', MaliciousClass); + + // Verify Object.prototype was not polluted + expect(testObj.polluted).toBeUndefined(); + expect({}.polluted).toBeUndefined(); + }); + + it('should not pollute Object.prototype when parsing JSON with __proto__ className', () => { + const testObj = {}; + + const maliciousJSON = { + className: '__proto__', + objectId: 'test123', + polluted: 'yes', + }; + + ParseObject.fromJSON(maliciousJSON); + + // Verify Object.prototype was not polluted + expect(testObj.polluted).toBeUndefined(); + expect({}.polluted).toBeUndefined(); + }); + + it('should not pollute Object.prototype when parsing JSON with constructor className', () => { + const testObj = {}; + + const maliciousJSON = { + className: 'constructor', + objectId: 'test456', + malicious: 'data', + }; + + ParseObject.fromJSON(maliciousJSON); + + // Verify Object.prototype was not polluted + expect(testObj.malicious).toBeUndefined(); + expect({}.malicious).toBeUndefined(); + }); + + it('should not pollute Object.prototype when parsing JSON with prototype className', () => { + const testObj = {}; + + const maliciousJSON = { + className: 'prototype', + objectId: 'test789', + exploit: 'here', + }; + + ParseObject.fromJSON(maliciousJSON); + + // Verify Object.prototype was not polluted + expect(testObj.exploit).toBeUndefined(); + expect({}.exploit).toBeUndefined(); + }); + + it('should not pollute when creating objects with malicious class names', () => { + const testObj = {}; + + const maliciousClasses = ['__proto__', 'constructor', 'prototype']; + + maliciousClasses.forEach(className => { + const obj = new ParseObject(className); + obj.set('polluted', 'yes'); + }); + + // Verify Object.prototype was not polluted + expect(testObj.polluted).toBeUndefined(); + expect({}.polluted).toBeUndefined(); + }); + + it('should not pollute when fromJSON is called multiple times with malicious classNames', () => { + const testObj = {}; + + const maliciousObjects = [ + { className: '__proto__', objectId: '1', polluted: 'yes' }, + { className: 'constructor', objectId: '2', malicious: 'data' }, + { className: 'prototype', objectId: '3', exploit: 'here' }, + ]; + + maliciousObjects.forEach(json => ParseObject.fromJSON(json)); + + // Verify Object.prototype was not polluted + expect(testObj.polluted).toBeUndefined(); + expect(testObj.malicious).toBeUndefined(); + expect(testObj.exploit).toBeUndefined(); + expect({}.polluted).toBeUndefined(); + expect({}.malicious).toBeUndefined(); + expect({}.exploit).toBeUndefined(); + }); + }); });
types/isDangerousKey.d.ts+7 −0 added@@ -0,0 +1,7 @@ +/** + * Check if a property name or path is potentially dangerous for prototype pollution + * Dangerous keys include: __proto__, constructor, prototype + * @param key - The property name or dotted path to check + * @returns true if the key is dangerous, false otherwise + */ +export declare function isDangerousKey(key: string): boolean;
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-9f2h-7v79-mxw3ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-62374ghsaADVISORY
- github.com/parse-community/Parse-SDK-JS/commit/00973987f361368659c0c4dbf669f3897520b132nvdWEB
- github.com/parse-community/Parse-SDK-JS/pull/2749nvdWEB
- github.com/parse-community/Parse-SDK-JS/releases/tag/7.0.0-alpha.1nvdWEB
- github.com/parse-community/Parse-SDK-JS/security/advisories/GHSA-9f2h-7v79-mxw3nvdWEB
News mentions
0No linked articles in our index yet.