From 5f02b5fdc85d134490dee5d32198d0e30f5bedc4 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 13 Jul 2026 14:29:26 +0200 Subject: [PATCH 1/2] test: Add _Installation non-master access control regression tests --- spec/ParseInstallation.spec.js | 76 ++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/spec/ParseInstallation.spec.js b/spec/ParseInstallation.spec.js index 261733c3af..00752d6b73 100644 --- a/spec/ParseInstallation.spec.js +++ b/spec/ParseInstallation.spec.js @@ -1298,6 +1298,82 @@ describe('Installations', () => { // TODO: Do we need to support _tombstone disabling of installations? // TODO: Test deletion, badge increments + describe('access control for non-master clients', () => { + const anonymousHeaders = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + + it('blocks the find operation for an unauthenticated client', async () => { + await rest.create(config, auth.nobody(config), '_Installation', { + installationId: '12345678-abcd-abcd-abcd-123456789abc', + deviceType: 'android', + }); + let error; + try { + await request({ + method: 'GET', + headers: anonymousHeaders, + url: 'http://localhost:8378/1/installations', + }); + fail('find should have been rejected'); + } catch (e) { + error = e; + } + expect(error.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); + expect(error.data.error).toBe('Permission denied'); + }); + + it('blocks the delete operation for an unauthenticated client', async () => { + const created = await rest.create(config, auth.nobody(config), '_Installation', { + installationId: '12345678-abcd-abcd-abcd-123456789abc', + deviceType: 'android', + }); + let error; + try { + await request({ + method: 'DELETE', + headers: anonymousHeaders, + url: 'http://localhost:8378/1/installations/' + created.response.objectId, + }); + fail('delete should have been rejected'); + } catch (e) { + error = e; + } + expect(error.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); + expect(error.data.error).toBe('Permission denied'); + // The row is still present: the anonymous delete did not take effect. + const remaining = await new Parse.Query(Parse.Installation).count({ useMasterKey: true }); + expect(remaining).toBe(1); + }); + + it('blocks the find operation for an authenticated non-master user', async () => { + // Even a logged-in user cannot enumerate installations, so another + // device's objectId cannot be discovered through an authenticated session. + const user = await Parse.User.signUp('installation-acl-user', 'pass-12345678'); + await rest.create(config, auth.nobody(config), '_Installation', { + installationId: '12345678-abcd-abcd-abcd-123456789abc', + deviceType: 'android', + }); + let error; + try { + await request({ + method: 'GET', + headers: { + ...anonymousHeaders, + 'X-Parse-Session-Token': user.getSessionToken(), + }, + url: 'http://localhost:8378/1/installations', + }); + fail('find should have been rejected'); + } catch (e) { + error = e; + } + expect(error.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); + expect(error.data.error).toBe('Permission denied'); + }); + }); + describe('deviceToken deduplication on new install (no installationId match)', () => { const { randomUUID } = require('crypto'); const installationSchema = { From 04b0a592caf71526f8e4123490ff1ded5e52bb30 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 14 Jul 2026 02:46:44 +0200 Subject: [PATCH 2/2] test: Add authenticated non-master delete case and guard rejection assertions --- spec/ParseInstallation.spec.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/spec/ParseInstallation.spec.js b/spec/ParseInstallation.spec.js index 00752d6b73..02f1591632 100644 --- a/spec/ParseInstallation.spec.js +++ b/spec/ParseInstallation.spec.js @@ -1317,6 +1317,7 @@ describe('Installations', () => { url: 'http://localhost:8378/1/installations', }); fail('find should have been rejected'); + return; } catch (e) { error = e; } @@ -1337,6 +1338,7 @@ describe('Installations', () => { url: 'http://localhost:8378/1/installations/' + created.response.objectId, }); fail('delete should have been rejected'); + return; } catch (e) { error = e; } @@ -1366,12 +1368,41 @@ describe('Installations', () => { url: 'http://localhost:8378/1/installations', }); fail('find should have been rejected'); + return; } catch (e) { error = e; } expect(error.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(error.data.error).toBe('Permission denied'); }); + + it('blocks the delete operation for an authenticated non-master user', async () => { + const user = await Parse.User.signUp('installation-acl-user', 'pass-12345678'); + const created = await rest.create(config, auth.nobody(config), '_Installation', { + installationId: '12345678-abcd-abcd-abcd-123456789abc', + deviceType: 'android', + }); + let error; + try { + await request({ + method: 'DELETE', + headers: { + ...anonymousHeaders, + 'X-Parse-Session-Token': user.getSessionToken(), + }, + url: 'http://localhost:8378/1/installations/' + created.response.objectId, + }); + fail('delete should have been rejected'); + return; + } catch (e) { + error = e; + } + expect(error.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); + expect(error.data.error).toBe('Permission denied'); + // The row is still present: the authenticated non-master delete did not take effect. + const remaining = await new Parse.Query(Parse.Installation).count({ useMasterKey: true }); + expect(remaining).toBe(1); + }); }); describe('deviceToken deduplication on new install (no installationId match)', () => {