Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions spec/vulnerabilities.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6101,6 +6101,66 @@ describe('Vulnerabilities', () => {
expect(contextAfterDelete).toBeDefined();
expect(contextAfterDelete.isAdmin).toBeUndefined();
});

it('does not expose Object.prototype on beforeFind trigger context', async () => {
// getRequestQueryObject builds the beforeFind trigger request. Its context must be
// prototype-isolated like every other trigger path (getRequestObject), so a polluted
// Object.prototype cannot leak into the request.context read by Cloud Code.
let contextProto;
let contextValue;
Parse.Cloud.beforeFind('ContextTest', req => {
contextProto = Object.getPrototypeOf(req.context);
contextValue = req.context.foo;
});
const query = new Parse.Query('ContextTest');
await query.find({ context: { foo: 'bar' } });
expect(contextValue).toBe('bar');
expect(contextProto).toBeNull();
});

it('isolates beforeFind trigger context from Object.prototype pollution', async () => {
// Simulate a separate prototype-pollution issue elsewhere in the process and verify the
// beforeFind trigger context does not inherit the polluted property.
const probe = '__parseServerBeforeFindContextProbe';
let inheritedProbe;
Parse.Cloud.beforeFind('ContextTest', req => {
inheritedProbe = req.context[probe];
});
Object.defineProperty(Object.prototype, probe, {
value: true,
configurable: true,
enumerable: false,
writable: true,
});
try {
const query = new Parse.Query('ContextTest');
await query.find({ context: { foo: 'bar' } });
} finally {
delete Object.prototype[probe];
}
expect(inheritedProbe).toBeUndefined();
});

it('propagates beforeFind context mutations to afterFind with prototype isolation', async () => {
// Regression guard for the copy + write-back fix: beforeFind and afterFind must still
// share context mutations (as documented), and both trigger contexts must be isolated.
let beforeFindProto;
let afterFindProto;
let afterFindValue;
Parse.Cloud.beforeFind('ContextTest', req => {
beforeFindProto = Object.getPrototypeOf(req.context);
req.context.injected = 'from-beforeFind';
});
Parse.Cloud.afterFind('ContextTest', req => {
afterFindProto = Object.getPrototypeOf(req.context);
afterFindValue = req.context.injected;
});
const query = new Parse.Query('ContextTest');
await query.find({ context: { foo: 'bar' } });
expect(beforeFindProto).toBeNull();
expect(afterFindProto).toBeNull();
expect(afterFindValue).toBe('from-beforeFind');
});
});

describe('(GHSA-hpm8-9qx6-jvwv) Ranged file download bypasses afterFind(Parse.File) trigger and validators', () => {
Expand Down
10 changes: 9 additions & 1 deletion src/triggers.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,9 @@ export function getRequestQueryObject(triggerType, auth, query, count, config, c
isGet,
headers: config.headers,
ip: config.ip,
context: context || {},
// Set a copy of the context on the request object, with a null prototype so a
// polluted Object.prototype cannot leak into the trigger context
context: Object.assign(Object.create(null), context || {}),
config,
};

Expand Down Expand Up @@ -612,6 +614,12 @@ export function maybeRunQueryTrigger(
})
.then(
result => {
// Propagate any context mutations made by the trigger back to the shared context,
// mirroring the write-back for other trigger types in maybeRunTrigger. This preserves
// beforeFind -> afterFind context propagation now that the request context is a copy.
if (context) {
Object.assign(context, requestObject.context);
}
let queryResult = parseQuery;
if (result && result instanceof Parse.Query) {
queryResult = result;
Expand Down
Loading