From 2b6b385afd113da41e8bffc3b2af172ec440e472 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 3 Mar 2026 20:30:45 +0000 Subject: [PATCH] fix --- spec/rest.spec.js | 135 +++++++++++++++++++++++++++++++++ src/Routers/FunctionsRouter.js | 8 ++ src/Routers/HooksRouter.js | 15 ++++ 3 files changed, 158 insertions(+) diff --git a/spec/rest.spec.js b/spec/rest.spec.js index 4d8f40a982..9e5a9858e4 100644 --- a/spec/rest.spec.js +++ b/spec/rest.spec.js @@ -1172,6 +1172,141 @@ describe('read-only masterKey', () => { done(); }); }); + + it('should throw when trying to create a hook function', async () => { + loggerErrorSpy.calls.reset(); + try { + await request({ + url: `${Parse.serverURL}/hooks/functions`, + method: 'POST', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': 'read-only-test', + 'Content-Type': 'application/json', + }, + body: { functionName: 'readOnlyTest', url: 'https://example.com/hook' }, + }); + fail('should have thrown'); + } catch (res) { + expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); + expect(res.data.error).toBe('Permission denied'); + } + }); + + it('should throw when trying to create a hook trigger', async () => { + loggerErrorSpy.calls.reset(); + try { + await request({ + url: `${Parse.serverURL}/hooks/triggers`, + method: 'POST', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': 'read-only-test', + 'Content-Type': 'application/json', + }, + body: { className: 'MyClass', triggerName: 'beforeSave', url: 'https://example.com/hook' }, + }); + fail('should have thrown'); + } catch (res) { + expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); + expect(res.data.error).toBe('Permission denied'); + } + }); + + it('should throw when trying to update a hook function', async () => { + // First create the hook with the real master key + await request({ + url: `${Parse.serverURL}/hooks/functions`, + method: 'POST', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + 'Content-Type': 'application/json', + }, + body: { functionName: 'readOnlyUpdateTest', url: 'https://example.com/hook' }, + }); + loggerErrorSpy.calls.reset(); + try { + await request({ + url: `${Parse.serverURL}/hooks/functions/readOnlyUpdateTest`, + method: 'PUT', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': 'read-only-test', + 'Content-Type': 'application/json', + }, + body: { url: 'https://example.com/hacked' }, + }); + fail('should have thrown'); + } catch (res) { + expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); + expect(res.data.error).toBe('Permission denied'); + } + }); + + it('should throw when trying to delete a hook function', async () => { + // First create the hook with the real master key + await request({ + url: `${Parse.serverURL}/hooks/functions`, + method: 'POST', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + 'Content-Type': 'application/json', + }, + body: { functionName: 'readOnlyDeleteTest', url: 'https://example.com/hook' }, + }); + loggerErrorSpy.calls.reset(); + try { + await request({ + url: `${Parse.serverURL}/hooks/functions/readOnlyDeleteTest`, + method: 'PUT', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': 'read-only-test', + 'Content-Type': 'application/json', + }, + body: { __op: 'Delete' }, + }); + fail('should have thrown'); + } catch (res) { + expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); + expect(res.data.error).toBe('Permission denied'); + } + }); + + it('should throw when trying to run a job with readOnlyMasterKey', async () => { + Parse.Cloud.job('readOnlyTestJob', () => {}); + loggerErrorSpy.calls.reset(); + try { + await request({ + url: `${Parse.serverURL}/jobs/readOnlyTestJob`, + method: 'POST', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': 'read-only-test', + 'Content-Type': 'application/json', + }, + body: {}, + }); + fail('should have thrown'); + } catch (res) { + expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); + expect(res.data.error).toBe('Permission denied'); + } + }); + + it('should allow reading hooks with readOnlyMasterKey', async () => { + const res = await request({ + url: `${Parse.serverURL}/hooks/functions`, + method: 'GET', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': 'read-only-test', + }, + }); + expect(Array.isArray(res.data)).toBe(true); + }); }); describe('rest context', () => { diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index f116cdc9a8..bfeed577ca 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -8,6 +8,7 @@ import { promiseEnforceMasterKeyAccess, promiseEnsureIdempotency } from '../midd import { jobStatusHandler } from '../StatusHandler'; import _ from 'lodash'; import { logger } from '../logger'; +import { createSanitizedError } from '../Error'; function parseObject(obj, config) { if (Array.isArray(obj)) { @@ -62,6 +63,13 @@ export class FunctionsRouter extends PromiseRouter { } static handleCloudJob(req) { + if (req.auth.isReadOnly) { + throw createSanitizedError( + Parse.Error.OPERATION_FORBIDDEN, + "read-only masterKey isn't allowed to run a job.", + req.config + ); + } const jobName = req.params.jobName || req.body?.jobName; const applicationId = req.config.applicationId; const jobHandler = jobStatusHandler(req.config); diff --git a/src/Routers/HooksRouter.js b/src/Routers/HooksRouter.js index 104ef799c2..5123efc381 100644 --- a/src/Routers/HooksRouter.js +++ b/src/Routers/HooksRouter.js @@ -1,6 +1,7 @@ import { Parse } from 'parse/node'; import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; +import { createSanitizedError } from '../Error'; export class HooksRouter extends PromiseRouter { createHook(aHook, config) { @@ -12,6 +13,13 @@ export class HooksRouter extends PromiseRouter { } handlePost(req) { + if (req.auth.isReadOnly) { + throw createSanitizedError( + Parse.Error.OPERATION_FORBIDDEN, + "read-only masterKey isn't allowed to create a hook.", + req.config + ); + } return this.createHook(req.body || {}, req.config); } @@ -82,6 +90,13 @@ export class HooksRouter extends PromiseRouter { } handlePut(req) { + if (req.auth.isReadOnly) { + throw createSanitizedError( + Parse.Error.OPERATION_FORBIDDEN, + "read-only masterKey isn't allowed to modify a hook.", + req.config + ); + } var body = req.body || {}; if (body.__op == 'Delete') { return this.handleDelete(req);