fix: Cloud Hooks and Cloud Jobs bypass readOnlyMasterKey write restriction (GHSA-vc89-5g3r-cmhh) (#10089)

This commit is contained in:
Manuel
2026-03-04 00:15:09 +00:00
committed by GitHub
parent 9f28ff5663
commit 6c79da91fc
3 changed files with 158 additions and 0 deletions
+135
View File
@@ -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', () => {
+8
View File
@@ -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)) {
@@ -58,6 +59,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);
+15
View File
@@ -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);