mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
fix: Cloud Hooks and Cloud Jobs bypass readOnlyMasterKey write restriction (GHSA-vc89-5g3r-cmhh) (#10089)
This commit is contained in:
@@ -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,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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user