From 6a7755ce924ca0bb06d62014a0a80a324f7ce791 Mon Sep 17 00:00:00 2001 From: faceapps Date: Thu, 19 Mar 2026 11:29:22 +0530 Subject: [PATCH] fix: `toJSONwithObjects` leaks `ParseACL` instances when afterFind hooks modify objects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When `directAccess` is enabled, `toJSONwithObjects` iterates pending ops and calls `object.get(key)` for each dirty field. For ACL, this returns a live `ParseACL` instance. Since `ParseACL` lacks `_toFullJSON`, the instance was assigned directly to the response JSON. With `directAccess`, the response bypasses HTTP serialization, so the raw `ParseACL` leaks to the client SDK. The client's `ParseObject._finishFetch` then calls `new ParseACL(aclData)` expecting plain JSON but receiving a `ParseACL` instance, throwing: "TypeError: Tried to create an ACL with an invalid permission type." The fix calls `val.toJSON()` on values that support it before assigning, ensuring SDK types like `ParseACL` and `ParseGeoPoint` are serialized to plain JSON — consistent with what `object.toJSON()` already does. Co-Authored-By: Claude Opus 4.6 (1M context) --- spec/CloudCode.spec.js | 67 ++++++++++++++++++++++++++++++++++++++++++ src/triggers.js | 2 +- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 45f3461bc4..a0be63d56f 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -3600,6 +3600,73 @@ describe('afterFind hooks', () => { expect(calledBefore).toBe(true); expect(calledAfter).toBe(true); }); + + it('toJSONwithObjects should serialize pending ACL to plain JSON, not a ParseACL instance', () => { + const { toJSONwithObjects } = require('../lib/triggers'); + + const obj = Parse.Object.fromJSON({ + className: 'Test', + objectId: 'test123', + ACL: { '*': { read: true }, 'role:admin': { read: true, write: true } }, + name: 'original', + }); + + const acl = new Parse.ACL(); + acl.setPublicReadAccess(true); + acl.setRoleWriteAccess('admin', true); + obj.setACL(acl); + + const json = toJSONwithObjects(obj, 'Test'); + + expect(json.ACL).toBeDefined(); + expect(json.ACL instanceof Parse.ACL).toBe(false); + expect(json.ACL.constructor).toBe(Object); + expect(json.ACL['*']).toEqual({ read: true }); + expect(json.ACL['role:admin']).toEqual({ write: true }); + }); + + it('should return valid ACL with directAccess enabled when afterFind hook calls setACL()', async () => { + const ParseServerRESTController = + require('../lib/ParseServerRESTController').ParseServerRESTController; + const DirectParseServer = require('../lib/ParseServer').default; + + await reconfigureServer({ directAccess: true }); + + Parse.CoreManager.setRESTController( + ParseServerRESTController( + Parse.applicationId, + DirectParseServer.promiseRouter({ appId: Parse.applicationId }) + ) + ); + + Parse.Cloud.afterFind('DirectAccessACL', req => { + req.objects.forEach(obj => { + obj.setACL(obj.getACL()); + obj.set('touched', true); + }); + return req.objects; + }); + + const obj = new Parse.Object('DirectAccessACL'); + const acl = new Parse.ACL(); + acl.setPublicReadAccess(true); + acl.setRoleWriteAccess('admin', true); + obj.setACL(acl); + obj.set('value', 42); + await obj.save(null, { useMasterKey: true }); + + const query = new Parse.Query('DirectAccessACL'); + const result = await query.first({ useMasterKey: true }); + + expect(result).toBeDefined(); + expect(result.get('value')).toBe(42); + expect(result.get('touched')).toBe(true); + + const resultACL = result.getACL(); + expect(resultACL).toBeDefined(); + expect(resultACL.getPublicReadAccess()).toBe(true); + expect(resultACL.getRoleWriteAccess('admin')).toBe(true); + }); }); describe('beforeLogin hook', () => { diff --git a/src/triggers.js b/src/triggers.js index 963382a007..671a0188b9 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -195,7 +195,7 @@ export function toJSONwithObjects(object, className) { for (const key in pending) { const val = object.get(key); if (!val || !val._toFullJSON) { - toJSON[key] = val; + toJSON[key] = val && typeof val.toJSON === 'function' ? val.toJSON() : val; continue; } toJSON[key] = val._toFullJSON();