Parse Server: LiveQuery bypasses CLP pointer permission enforcement
Description
Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js. Prior to versions 8.6.53 and 9.6.0-alpha.42, Parse Server's LiveQuery WebSocket interface does not enforce Class-Level Permission (CLP) pointer permissions (readUserFields and pointerFields). Any authenticated user can subscribe to LiveQuery events and receive real-time updates for all objects in classes protected by pointer permissions, regardless of whether the pointer fields on those objects point to the subscribing user. This bypasses the intended read access control, allowing unauthorized access to potentially sensitive data that is correctly restricted via the REST API. This issue has been patched in versions 8.6.53 and 9.6.0-alpha.42.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
parse-servernpm | >= 9.0.0, < 9.6.0-alpha.42 | 9.6.0-alpha.42 |
parse-servernpm | < 8.6.53 | 8.6.53 |
Affected products
1- Range: < 8.6.53
Patches
26c3317aca6ebfix: LiveQuery bypasses CLP pointer permission enforcement ([GHSA-fph2-r4qg-9576](https://github.com/parse-community/parse-server/security/advisories/GHSA-fph2-r4qg-9576)) (#10250)
2 files changed · +372 −3
spec/vulnerabilities.spec.js+301 −0 modified@@ -3349,3 +3349,304 @@ describe('(GHSA-5hmj-jcgp-6hff) Protected fields leak via LiveQuery afterEvent t }); }); }); + +describe('(GHSA-fph2-r4qg-9576) LiveQuery bypasses CLP pointer permission enforcement', () => { + const { sleep } = require('../lib/TestUtils'); + + beforeEach(() => { + Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null); + }); + + afterEach(async () => { + try { + const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); + if (client) { + await client.close(); + } + } catch (e) { + // Ignore cleanup errors when client is not initialized + } + }); + + async function updateCLP(className, permissions) { + const response = await fetch(Parse.serverURL + '/schemas/' + className, { + method: 'PUT', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ classLevelPermissions: permissions }), + }); + const body = await response.json(); + if (body.error) { + throw body; + } + return body; + } + + it('should not deliver LiveQuery events to user not in readUserFields pointer', async () => { + await reconfigureServer({ + liveQuery: { classNames: ['PrivateMessage'] }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + // Create users using master key to avoid session management issues + const userA = new Parse.User(); + userA.setUsername('userA_pointer'); + userA.setPassword('password123'); + await userA.signUp(); + await Parse.User.logOut(); + + // User B stays logged in for the subscription + const userB = new Parse.User(); + userB.setUsername('userB_pointer'); + userB.setPassword('password456'); + await userB.signUp(); + + // Create schema by saving an object with owner pointer, then set CLP + const seed = new Parse.Object('PrivateMessage'); + seed.set('owner', userA); + await seed.save(null, { useMasterKey: true }); + await seed.destroy({ useMasterKey: true }); + + await updateCLP('PrivateMessage', { + create: { '*': true }, + find: {}, + get: {}, + readUserFields: ['owner'], + }); + + // User B subscribes — should NOT receive events for User A's objects + const query = new Parse.Query('PrivateMessage'); + const subscription = await query.subscribe(userB.getSessionToken()); + + const createSpy = jasmine.createSpy('create'); + const enterSpy = jasmine.createSpy('enter'); + subscription.on('create', createSpy); + subscription.on('enter', enterSpy); + + // Create a message owned by User A + const msg = new Parse.Object('PrivateMessage'); + msg.set('content', 'secret message'); + msg.set('owner', userA); + await msg.save(null, { useMasterKey: true }); + + await sleep(500); + + // User B should NOT have received the create event + expect(createSpy).not.toHaveBeenCalled(); + expect(enterSpy).not.toHaveBeenCalled(); + }); + + it('should deliver LiveQuery events to user in readUserFields pointer', async () => { + await reconfigureServer({ + liveQuery: { classNames: ['PrivateMessage2'] }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + // User A stays logged in for the subscription + const userA = new Parse.User(); + userA.setUsername('userA_owner'); + userA.setPassword('password123'); + await userA.signUp(); + + // Create schema by saving an object with owner pointer + const seed = new Parse.Object('PrivateMessage2'); + seed.set('owner', userA); + await seed.save(null, { useMasterKey: true }); + await seed.destroy({ useMasterKey: true }); + + await updateCLP('PrivateMessage2', { + create: { '*': true }, + find: {}, + get: {}, + readUserFields: ['owner'], + }); + + // User A subscribes — SHOULD receive events for their own objects + const query = new Parse.Query('PrivateMessage2'); + const subscription = await query.subscribe(userA.getSessionToken()); + + const createSpy = jasmine.createSpy('create'); + subscription.on('create', createSpy); + + // Create a message owned by User A + const msg = new Parse.Object('PrivateMessage2'); + msg.set('content', 'my own message'); + msg.set('owner', userA); + await msg.save(null, { useMasterKey: true }); + + await sleep(500); + + // User A SHOULD have received the create event + expect(createSpy).toHaveBeenCalledTimes(1); + }); + + it('should not deliver LiveQuery events when find uses pointerFields', async () => { + await reconfigureServer({ + liveQuery: { classNames: ['PrivateDoc'] }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + const userA = new Parse.User(); + userA.setUsername('userA_doc'); + userA.setPassword('password123'); + await userA.signUp(); + await Parse.User.logOut(); + + // User B stays logged in for the subscription + const userB = new Parse.User(); + userB.setUsername('userB_doc'); + userB.setPassword('password456'); + await userB.signUp(); + + // Create schema by saving an object with recipient pointer + const seed = new Parse.Object('PrivateDoc'); + seed.set('recipient', userA); + await seed.save(null, { useMasterKey: true }); + await seed.destroy({ useMasterKey: true }); + + // Set CLP with pointerFields instead of readUserFields + await updateCLP('PrivateDoc', { + create: { '*': true }, + find: { pointerFields: ['recipient'] }, + get: { pointerFields: ['recipient'] }, + }); + + // User B subscribes + const query = new Parse.Query('PrivateDoc'); + const subscription = await query.subscribe(userB.getSessionToken()); + + const createSpy = jasmine.createSpy('create'); + subscription.on('create', createSpy); + + // Create doc with recipient = User A (not User B) + const doc = new Parse.Object('PrivateDoc'); + doc.set('title', 'confidential'); + doc.set('recipient', userA); + await doc.save(null, { useMasterKey: true }); + + await sleep(500); + + // User B should NOT receive events for User A's document + expect(createSpy).not.toHaveBeenCalled(); + }); + + it('should not deliver LiveQuery events to unauthenticated users for pointer-protected classes', async () => { + await reconfigureServer({ + liveQuery: { classNames: ['SecureItem'] }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + const userA = new Parse.User(); + userA.setUsername('userA_secure'); + userA.setPassword('password123'); + await userA.signUp(); + await Parse.User.logOut(); + + // Create schema + const seed = new Parse.Object('SecureItem'); + seed.set('owner', userA); + await seed.save(null, { useMasterKey: true }); + await seed.destroy({ useMasterKey: true }); + + await updateCLP('SecureItem', { + create: { '*': true }, + find: {}, + get: {}, + readUserFields: ['owner'], + }); + + // Unauthenticated subscription + const query = new Parse.Query('SecureItem'); + const subscription = await query.subscribe(); + + const createSpy = jasmine.createSpy('create'); + subscription.on('create', createSpy); + + const item = new Parse.Object('SecureItem'); + item.set('data', 'private'); + item.set('owner', userA); + await item.save(null, { useMasterKey: true }); + + await sleep(500); + + expect(createSpy).not.toHaveBeenCalled(); + }); + + it('should handle readUserFields with array of pointers', async () => { + await reconfigureServer({ + liveQuery: { classNames: ['SharedDoc'] }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + const userA = new Parse.User(); + userA.setUsername('userA_shared'); + userA.setPassword('password123'); + await userA.signUp(); + await Parse.User.logOut(); + + // User B — don't log out, session must remain valid + const userB = new Parse.User(); + userB.setUsername('userB_shared'); + userB.setPassword('password456'); + await userB.signUp(); + const userBSessionToken = userB.getSessionToken(); + + // User C — signUp changes current user to C, but B's session stays valid + const userC = new Parse.User(); + userC.setUsername('userC_shared'); + userC.setPassword('password789'); + await userC.signUp(); + const userCSessionToken = userC.getSessionToken(); + + // Create schema with array field + const seed = new Parse.Object('SharedDoc'); + seed.set('collaborators', [userA]); + await seed.save(null, { useMasterKey: true }); + await seed.destroy({ useMasterKey: true }); + + await updateCLP('SharedDoc', { + create: { '*': true }, + find: {}, + get: {}, + readUserFields: ['collaborators'], + }); + + // User B subscribes — is in the collaborators array + const queryB = new Parse.Query('SharedDoc'); + const subscriptionB = await queryB.subscribe(userBSessionToken); + const createSpyB = jasmine.createSpy('createB'); + subscriptionB.on('create', createSpyB); + + // User C subscribes — is NOT in the collaborators array + const queryC = new Parse.Query('SharedDoc'); + const subscriptionC = await queryC.subscribe(userCSessionToken); + const createSpyC = jasmine.createSpy('createC'); + subscriptionC.on('create', createSpyC); + + // Create doc with collaborators = [userA, userB] (not userC) + const doc = new Parse.Object('SharedDoc'); + doc.set('title', 'team doc'); + doc.set('collaborators', [userA, userB]); + await doc.save(null, { useMasterKey: true }); + + await sleep(500); + + // User B SHOULD receive the event (in collaborators array) + expect(createSpyB).toHaveBeenCalledTimes(1); + // User C should NOT receive the event + expect(createSpyC).not.toHaveBeenCalled(); + }); +});
src/LiveQuery/ParseLiveQueryServer.ts+71 −3 modified@@ -211,13 +211,16 @@ class ParseLiveQueryServer { const op = this._getCLPOperation(subscription.query); let res: any = {}; try { - await this._matchesCLP( + const matchesCLP = await this._matchesCLP( classLevelPermissions, message.currentParseObject, client, requestId, op ); + if (matchesCLP === false) { + return null; + } const isMatched = await this._matchesACL(acl, client, requestId); if (!isMatched) { return null; @@ -339,13 +342,16 @@ class ParseLiveQueryServer { } try { const op = this._getCLPOperation(subscription.query); - await this._matchesCLP( + const matchesCLP = await this._matchesCLP( classLevelPermissions, message.currentParseObject, client, requestId, op ); + if (matchesCLP === false) { + return; + } const [isOriginalMatched, isCurrentMatched] = await Promise.all([ originalACLCheckingPromise, currentACLCheckingPromise, @@ -659,8 +665,10 @@ class ParseLiveQueryServer { ): Promise<any> { const subscriptionInfo = client.getSubscriptionInfo(requestId); const aclGroup = ['*']; + let userId; if (typeof subscriptionInfo !== 'undefined') { - const { userId } = await this.getAuthForSessionToken(subscriptionInfo.sessionToken); + const result = await this.getAuthForSessionToken(subscriptionInfo.sessionToken); + userId = result.userId; if (userId) { aclGroup.push(userId); } @@ -671,6 +679,66 @@ class ParseLiveQueryServer { aclGroup, op ); + // Enforce pointer permissions that validatePermission defers. + // Returns false to silently skip the event (like ACL), rather than + // throwing which would push errors to the client and log noise. + if (!client.hasMasterKey && classLevelPermissions) { + const permissionField = + ['get', 'find', 'count'].indexOf(op) > -1 ? 'readUserFields' : 'writeUserFields'; + const pointerFields = []; + if (classLevelPermissions[op]?.pointerFields) { + pointerFields.push(...classLevelPermissions[op].pointerFields); + } + if (Array.isArray(classLevelPermissions[permissionField])) { + for (const field of classLevelPermissions[permissionField]) { + if (!pointerFields.includes(field)) { + pointerFields.push(field); + } + } + } + if (pointerFields.length > 0) { + // If public or user-specific permission already grants access, skip pointer check + if ( + !SchemaController.testPermissions(classLevelPermissions, aclGroup, op) + ) { + if (!userId) { + return false; + } + // Check if any pointer field points to the current user + const hasAccess = pointerFields.some(field => { + const value = + typeof object.get === 'function' ? object.get(field) : object[field]; + if (!value) { + return false; + } + // Handle Parse.Object pointer (has .id) + if (value.id) { + return value.id === userId; + } + // Handle raw pointer JSON (has .objectId) + if (value.objectId) { + return value.objectId === userId; + } + // Handle array of pointers + if (Array.isArray(value)) { + return value.some(item => { + if (item.id) { + return item.id === userId; + } + if (item.objectId) { + return item.objectId === userId; + } + return false; + }); + } + return false; + }); + if (!hasAccess) { + return false; + } + } + } + } } async _filterSensitiveData(
976dad109f3ffix: LiveQuery bypasses CLP pointer permission enforcement ([GHSA-fph2-r4qg-9576](https://github.com/parse-community/parse-server/security/advisories/GHSA-fph2-r4qg-9576)) (#10252)
2 files changed · +372 −3
spec/vulnerabilities.spec.js+301 −0 modified@@ -3115,3 +3115,304 @@ describe('(GHSA-5hmj-jcgp-6hff) Protected fields leak via LiveQuery afterEvent t }); }); }); + +describe('(GHSA-fph2-r4qg-9576) LiveQuery bypasses CLP pointer permission enforcement', () => { + const { sleep } = require('../lib/TestUtils'); + + beforeEach(() => { + Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null); + }); + + afterEach(async () => { + try { + const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); + if (client) { + await client.close(); + } + } catch (e) { + // Ignore cleanup errors when client is not initialized + } + }); + + async function updateCLP(className, permissions) { + const response = await fetch(Parse.serverURL + '/schemas/' + className, { + method: 'PUT', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ classLevelPermissions: permissions }), + }); + const body = await response.json(); + if (body.error) { + throw body; + } + return body; + } + + it('should not deliver LiveQuery events to user not in readUserFields pointer', async () => { + await reconfigureServer({ + liveQuery: { classNames: ['PrivateMessage'] }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + // Create users using master key to avoid session management issues + const userA = new Parse.User(); + userA.setUsername('userA_pointer'); + userA.setPassword('password123'); + await userA.signUp(); + await Parse.User.logOut(); + + // User B stays logged in for the subscription + const userB = new Parse.User(); + userB.setUsername('userB_pointer'); + userB.setPassword('password456'); + await userB.signUp(); + + // Create schema by saving an object with owner pointer, then set CLP + const seed = new Parse.Object('PrivateMessage'); + seed.set('owner', userA); + await seed.save(null, { useMasterKey: true }); + await seed.destroy({ useMasterKey: true }); + + await updateCLP('PrivateMessage', { + create: { '*': true }, + find: {}, + get: {}, + readUserFields: ['owner'], + }); + + // User B subscribes — should NOT receive events for User A's objects + const query = new Parse.Query('PrivateMessage'); + const subscription = await query.subscribe(userB.getSessionToken()); + + const createSpy = jasmine.createSpy('create'); + const enterSpy = jasmine.createSpy('enter'); + subscription.on('create', createSpy); + subscription.on('enter', enterSpy); + + // Create a message owned by User A + const msg = new Parse.Object('PrivateMessage'); + msg.set('content', 'secret message'); + msg.set('owner', userA); + await msg.save(null, { useMasterKey: true }); + + await sleep(500); + + // User B should NOT have received the create event + expect(createSpy).not.toHaveBeenCalled(); + expect(enterSpy).not.toHaveBeenCalled(); + }); + + it('should deliver LiveQuery events to user in readUserFields pointer', async () => { + await reconfigureServer({ + liveQuery: { classNames: ['PrivateMessage2'] }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + // User A stays logged in for the subscription + const userA = new Parse.User(); + userA.setUsername('userA_owner'); + userA.setPassword('password123'); + await userA.signUp(); + + // Create schema by saving an object with owner pointer + const seed = new Parse.Object('PrivateMessage2'); + seed.set('owner', userA); + await seed.save(null, { useMasterKey: true }); + await seed.destroy({ useMasterKey: true }); + + await updateCLP('PrivateMessage2', { + create: { '*': true }, + find: {}, + get: {}, + readUserFields: ['owner'], + }); + + // User A subscribes — SHOULD receive events for their own objects + const query = new Parse.Query('PrivateMessage2'); + const subscription = await query.subscribe(userA.getSessionToken()); + + const createSpy = jasmine.createSpy('create'); + subscription.on('create', createSpy); + + // Create a message owned by User A + const msg = new Parse.Object('PrivateMessage2'); + msg.set('content', 'my own message'); + msg.set('owner', userA); + await msg.save(null, { useMasterKey: true }); + + await sleep(500); + + // User A SHOULD have received the create event + expect(createSpy).toHaveBeenCalledTimes(1); + }); + + it('should not deliver LiveQuery events when find uses pointerFields', async () => { + await reconfigureServer({ + liveQuery: { classNames: ['PrivateDoc'] }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + const userA = new Parse.User(); + userA.setUsername('userA_doc'); + userA.setPassword('password123'); + await userA.signUp(); + await Parse.User.logOut(); + + // User B stays logged in for the subscription + const userB = new Parse.User(); + userB.setUsername('userB_doc'); + userB.setPassword('password456'); + await userB.signUp(); + + // Create schema by saving an object with recipient pointer + const seed = new Parse.Object('PrivateDoc'); + seed.set('recipient', userA); + await seed.save(null, { useMasterKey: true }); + await seed.destroy({ useMasterKey: true }); + + // Set CLP with pointerFields instead of readUserFields + await updateCLP('PrivateDoc', { + create: { '*': true }, + find: { pointerFields: ['recipient'] }, + get: { pointerFields: ['recipient'] }, + }); + + // User B subscribes + const query = new Parse.Query('PrivateDoc'); + const subscription = await query.subscribe(userB.getSessionToken()); + + const createSpy = jasmine.createSpy('create'); + subscription.on('create', createSpy); + + // Create doc with recipient = User A (not User B) + const doc = new Parse.Object('PrivateDoc'); + doc.set('title', 'confidential'); + doc.set('recipient', userA); + await doc.save(null, { useMasterKey: true }); + + await sleep(500); + + // User B should NOT receive events for User A's document + expect(createSpy).not.toHaveBeenCalled(); + }); + + it('should not deliver LiveQuery events to unauthenticated users for pointer-protected classes', async () => { + await reconfigureServer({ + liveQuery: { classNames: ['SecureItem'] }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + const userA = new Parse.User(); + userA.setUsername('userA_secure'); + userA.setPassword('password123'); + await userA.signUp(); + await Parse.User.logOut(); + + // Create schema + const seed = new Parse.Object('SecureItem'); + seed.set('owner', userA); + await seed.save(null, { useMasterKey: true }); + await seed.destroy({ useMasterKey: true }); + + await updateCLP('SecureItem', { + create: { '*': true }, + find: {}, + get: {}, + readUserFields: ['owner'], + }); + + // Unauthenticated subscription + const query = new Parse.Query('SecureItem'); + const subscription = await query.subscribe(); + + const createSpy = jasmine.createSpy('create'); + subscription.on('create', createSpy); + + const item = new Parse.Object('SecureItem'); + item.set('data', 'private'); + item.set('owner', userA); + await item.save(null, { useMasterKey: true }); + + await sleep(500); + + expect(createSpy).not.toHaveBeenCalled(); + }); + + it('should handle readUserFields with array of pointers', async () => { + await reconfigureServer({ + liveQuery: { classNames: ['SharedDoc'] }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + const userA = new Parse.User(); + userA.setUsername('userA_shared'); + userA.setPassword('password123'); + await userA.signUp(); + await Parse.User.logOut(); + + // User B — don't log out, session must remain valid + const userB = new Parse.User(); + userB.setUsername('userB_shared'); + userB.setPassword('password456'); + await userB.signUp(); + const userBSessionToken = userB.getSessionToken(); + + // User C — signUp changes current user to C, but B's session stays valid + const userC = new Parse.User(); + userC.setUsername('userC_shared'); + userC.setPassword('password789'); + await userC.signUp(); + const userCSessionToken = userC.getSessionToken(); + + // Create schema with array field + const seed = new Parse.Object('SharedDoc'); + seed.set('collaborators', [userA]); + await seed.save(null, { useMasterKey: true }); + await seed.destroy({ useMasterKey: true }); + + await updateCLP('SharedDoc', { + create: { '*': true }, + find: {}, + get: {}, + readUserFields: ['collaborators'], + }); + + // User B subscribes — is in the collaborators array + const queryB = new Parse.Query('SharedDoc'); + const subscriptionB = await queryB.subscribe(userBSessionToken); + const createSpyB = jasmine.createSpy('createB'); + subscriptionB.on('create', createSpyB); + + // User C subscribes — is NOT in the collaborators array + const queryC = new Parse.Query('SharedDoc'); + const subscriptionC = await queryC.subscribe(userCSessionToken); + const createSpyC = jasmine.createSpy('createC'); + subscriptionC.on('create', createSpyC); + + // Create doc with collaborators = [userA, userB] (not userC) + const doc = new Parse.Object('SharedDoc'); + doc.set('title', 'team doc'); + doc.set('collaborators', [userA, userB]); + await doc.save(null, { useMasterKey: true }); + + await sleep(500); + + // User B SHOULD receive the event (in collaborators array) + expect(createSpyB).toHaveBeenCalledTimes(1); + // User C should NOT receive the event + expect(createSpyC).not.toHaveBeenCalled(); + }); +});
src/LiveQuery/ParseLiveQueryServer.ts+71 −3 modified@@ -211,13 +211,16 @@ class ParseLiveQueryServer { const op = this._getCLPOperation(subscription.query); let res: any = {}; try { - await this._matchesCLP( + const matchesCLP = await this._matchesCLP( classLevelPermissions, message.currentParseObject, client, requestId, op ); + if (matchesCLP === false) { + return null; + } const isMatched = await this._matchesACL(acl, client, requestId); if (!isMatched) { return null; @@ -339,13 +342,16 @@ class ParseLiveQueryServer { } try { const op = this._getCLPOperation(subscription.query); - await this._matchesCLP( + const matchesCLP = await this._matchesCLP( classLevelPermissions, message.currentParseObject, client, requestId, op ); + if (matchesCLP === false) { + return; + } const [isOriginalMatched, isCurrentMatched] = await Promise.all([ originalACLCheckingPromise, currentACLCheckingPromise, @@ -659,8 +665,10 @@ class ParseLiveQueryServer { ): Promise<any> { const subscriptionInfo = client.getSubscriptionInfo(requestId); const aclGroup = ['*']; + let userId; if (typeof subscriptionInfo !== 'undefined') { - const { userId } = await this.getAuthForSessionToken(subscriptionInfo.sessionToken); + const result = await this.getAuthForSessionToken(subscriptionInfo.sessionToken); + userId = result.userId; if (userId) { aclGroup.push(userId); } @@ -671,6 +679,66 @@ class ParseLiveQueryServer { aclGroup, op ); + // Enforce pointer permissions that validatePermission defers. + // Returns false to silently skip the event (like ACL), rather than + // throwing which would push errors to the client and log noise. + if (!client.hasMasterKey && classLevelPermissions) { + const permissionField = + ['get', 'find', 'count'].indexOf(op) > -1 ? 'readUserFields' : 'writeUserFields'; + const pointerFields = []; + if (classLevelPermissions[op]?.pointerFields) { + pointerFields.push(...classLevelPermissions[op].pointerFields); + } + if (Array.isArray(classLevelPermissions[permissionField])) { + for (const field of classLevelPermissions[permissionField]) { + if (!pointerFields.includes(field)) { + pointerFields.push(field); + } + } + } + if (pointerFields.length > 0) { + // If public or user-specific permission already grants access, skip pointer check + if ( + !SchemaController.testPermissions(classLevelPermissions, aclGroup, op) + ) { + if (!userId) { + return false; + } + // Check if any pointer field points to the current user + const hasAccess = pointerFields.some(field => { + const value = + typeof object.get === 'function' ? object.get(field) : object[field]; + if (!value) { + return false; + } + // Handle Parse.Object pointer (has .id) + if (value.id) { + return value.id === userId; + } + // Handle raw pointer JSON (has .objectId) + if (value.objectId) { + return value.objectId === userId; + } + // Handle array of pointers + if (Array.isArray(value)) { + return value.some(item => { + if (item.id) { + return item.id === userId; + } + if (item.objectId) { + return item.objectId === userId; + } + return false; + }); + } + return false; + }); + if (!hasAccess) { + return false; + } + } + } + } } async _filterSensitiveData(
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
7- github.com/advisories/GHSA-fph2-r4qg-9576ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-33421ghsaADVISORY
- github.com/parse-community/parse-server/commit/6c3317aca6eb618ac48f999021ae3ef7766ad1eaghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/commit/976dad109f3fe3fbd0a3a35ef62e7a5d35eb0beeghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/pull/10250ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/pull/10252ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/security/advisories/GHSA-fph2-r4qg-9576ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.