mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
fix: Denial of Service (DoS) and Cloud Function Dispatch Bypass via Prototype Chain Resolution ([GHSA-5j86-7r7m-p8h6](https://github.com/parse-community/parse-server/security/advisories/GHSA-5j86-7r7m-p8h6)) (#10124)
This commit is contained in:
@@ -131,6 +131,91 @@ describe('Vulnerabilities', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('(GHSA-5j86-7r7m-p8h6) Cloud function name prototype chain bypass', () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
|
||||
it('rejects "constructor" as cloud function name', async () => {
|
||||
const response = await request({
|
||||
headers,
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/functions/constructor',
|
||||
body: JSON.stringify({}),
|
||||
}).catch(e => e);
|
||||
expect(response.status).toBe(400);
|
||||
const text = JSON.parse(response.text);
|
||||
expect(text.code).toBe(Parse.Error.SCRIPT_FAILED);
|
||||
expect(text.error).toContain('Invalid function');
|
||||
});
|
||||
|
||||
it('rejects "toString" as cloud function name', async () => {
|
||||
const response = await request({
|
||||
headers,
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/functions/toString',
|
||||
body: JSON.stringify({}),
|
||||
}).catch(e => e);
|
||||
expect(response.status).toBe(400);
|
||||
const text = JSON.parse(response.text);
|
||||
expect(text.code).toBe(Parse.Error.SCRIPT_FAILED);
|
||||
expect(text.error).toContain('Invalid function');
|
||||
});
|
||||
|
||||
it('rejects "valueOf" as cloud function name', async () => {
|
||||
const response = await request({
|
||||
headers,
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/functions/valueOf',
|
||||
body: JSON.stringify({}),
|
||||
}).catch(e => e);
|
||||
expect(response.status).toBe(400);
|
||||
const text = JSON.parse(response.text);
|
||||
expect(text.code).toBe(Parse.Error.SCRIPT_FAILED);
|
||||
expect(text.error).toContain('Invalid function');
|
||||
});
|
||||
|
||||
it('rejects "hasOwnProperty" as cloud function name', async () => {
|
||||
const response = await request({
|
||||
headers,
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/functions/hasOwnProperty',
|
||||
body: JSON.stringify({}),
|
||||
}).catch(e => e);
|
||||
expect(response.status).toBe(400);
|
||||
const text = JSON.parse(response.text);
|
||||
expect(text.code).toBe(Parse.Error.SCRIPT_FAILED);
|
||||
expect(text.error).toContain('Invalid function');
|
||||
});
|
||||
|
||||
it('rejects "__proto__.toString" as cloud function name', async () => {
|
||||
const response = await request({
|
||||
headers,
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/functions/__proto__.toString',
|
||||
body: JSON.stringify({}),
|
||||
}).catch(e => e);
|
||||
expect(response.status).toBe(400);
|
||||
const text = JSON.parse(response.text);
|
||||
expect(text.code).toBe(Parse.Error.SCRIPT_FAILED);
|
||||
expect(text.error).toContain('Invalid function');
|
||||
});
|
||||
|
||||
it('still executes a legitimately defined cloud function', async () => {
|
||||
Parse.Cloud.define('legitimateFunction', () => 'hello');
|
||||
const response = await request({
|
||||
headers,
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/functions/legitimateFunction',
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
expect(JSON.parse(response.text).result).toBe('hello');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Request denylist', () => {
|
||||
describe('(GHSA-q342-9w2p-57fp) Denylist bypass via sibling nested objects', () => {
|
||||
it('denies _bsontype:Code after a sibling nested object', async () => {
|
||||
|
||||
+18
-8
@@ -20,18 +20,28 @@ export const Types = {
|
||||
|
||||
const ConnectClassName = '@Connect';
|
||||
|
||||
/**
|
||||
* Creates a prototype-free object for use as a lookup store.
|
||||
* This prevents prototype chain properties (e.g. `constructor`, `toString`)
|
||||
* from being resolved as registered handlers when using bracket notation
|
||||
* for lookups. Always use this instead of `{}` for handler stores.
|
||||
*/
|
||||
function createStore() {
|
||||
return Object.create(null);
|
||||
}
|
||||
|
||||
const baseStore = function () {
|
||||
const Validators = Object.keys(Types).reduce(function (base, key) {
|
||||
base[key] = {};
|
||||
base[key] = createStore();
|
||||
return base;
|
||||
}, {});
|
||||
const Functions = {};
|
||||
const Jobs = {};
|
||||
}, createStore());
|
||||
const Functions = createStore();
|
||||
const Jobs = createStore();
|
||||
const LiveQuery = [];
|
||||
const Triggers = Object.keys(Types).reduce(function (base, key) {
|
||||
base[key] = {};
|
||||
base[key] = createStore();
|
||||
return base;
|
||||
}, {});
|
||||
}, createStore());
|
||||
|
||||
return Object.freeze({
|
||||
Functions,
|
||||
@@ -90,7 +100,7 @@ function getStore(category, name, applicationId) {
|
||||
const invalidNameRegex = /['"`]/;
|
||||
if (invalidNameRegex.test(name)) {
|
||||
// Prevent a malicious user from injecting properties into the store
|
||||
return {};
|
||||
return createStore();
|
||||
}
|
||||
|
||||
const path = name.split('.');
|
||||
@@ -101,7 +111,7 @@ function getStore(category, name, applicationId) {
|
||||
for (const component of path) {
|
||||
store = store[component];
|
||||
if (!store) {
|
||||
return {};
|
||||
return createStore();
|
||||
}
|
||||
}
|
||||
return store;
|
||||
|
||||
Reference in New Issue
Block a user