fix: Protected fields leak via LiveQuery afterEvent trigger ([GHSA-5hmj-jcgp-6hff](https://github.com/parse-community/parse-server/security/advisories/GHSA-5hmj-jcgp-6hff)) (#10232)

This commit is contained in:
Manuel
2026-03-17 18:34:30 +00:00
committed by GitHub
parent 0a6c872c2c
commit 6648500428
2 changed files with 136 additions and 0 deletions
+130
View File
@@ -3092,3 +3092,133 @@ describe('(GHSA-fjxm-vhvc-gcmj) LiveQuery Operator Type Confusion', () => {
});
});
});
describe('(GHSA-5hmj-jcgp-6hff) Protected fields leak via LiveQuery afterEvent trigger', () => {
let obj;
beforeEach(async () => {
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
await reconfigureServer({
liveQuery: { classNames: ['SecretClass'] },
startLiveQueryServer: true,
verbose: false,
silent: true,
});
Parse.Cloud.afterLiveQueryEvent('SecretClass', () => {});
const config = Config.get(Parse.applicationId);
const schemaController = await config.database.loadSchema();
await schemaController.addClassIfNotExists('SecretClass', {
secretField: { type: 'String' },
publicField: { type: 'String' },
});
await schemaController.updateClass(
'SecretClass',
{},
{
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: {},
protectedFields: { '*': ['secretField'] },
}
);
obj = new Parse.Object('SecretClass');
obj.set('secretField', 'SENSITIVE_DATA');
obj.set('publicField', 'visible');
await obj.save(null, { useMasterKey: true });
});
afterEach(async () => {
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
if (client) {
await client.close();
}
});
it('should not leak protected fields on update event when afterEvent trigger is registered', async () => {
const query = new Parse.Query('SecretClass');
const subscription = await query.subscribe();
await Promise.all([
new Promise(resolve => {
subscription.on('update', (object, original) => {
expect(object.get('secretField')).toBeUndefined();
expect(object.get('publicField')).toBe('updated');
expect(original.get('secretField')).toBeUndefined();
expect(original.get('publicField')).toBe('visible');
resolve();
});
}),
obj.save({ publicField: 'updated' }, { useMasterKey: true }),
]);
});
it('should not leak protected fields on create event when afterEvent trigger is registered', async () => {
const query = new Parse.Query('SecretClass');
const subscription = await query.subscribe();
await Promise.all([
new Promise(resolve => {
subscription.on('create', object => {
expect(object.get('secretField')).toBeUndefined();
expect(object.get('publicField')).toBe('new');
resolve();
});
}),
new Parse.Object('SecretClass').save(
{ secretField: 'SECRET', publicField: 'new' },
{ useMasterKey: true }
),
]);
});
it('should not leak protected fields on delete event when afterEvent trigger is registered', async () => {
const query = new Parse.Query('SecretClass');
const subscription = await query.subscribe();
await Promise.all([
new Promise(resolve => {
subscription.on('delete', object => {
expect(object.get('secretField')).toBeUndefined();
expect(object.get('publicField')).toBe('visible');
resolve();
});
}),
obj.destroy({ useMasterKey: true }),
]);
});
it('should not leak protected fields on enter event when afterEvent trigger is registered', async () => {
const query = new Parse.Query('SecretClass');
query.equalTo('publicField', 'match');
const subscription = await query.subscribe();
await Promise.all([
new Promise(resolve => {
subscription.on('enter', (object, original) => {
expect(object.get('secretField')).toBeUndefined();
expect(object.get('publicField')).toBe('match');
expect(original.get('secretField')).toBeUndefined();
resolve();
});
}),
obj.save({ publicField: 'match' }, { useMasterKey: true }),
]);
});
it('should not leak protected fields on leave event when afterEvent trigger is registered', async () => {
const query = new Parse.Query('SecretClass');
query.equalTo('publicField', 'visible');
const subscription = await query.subscribe();
await Promise.all([
new Promise(resolve => {
subscription.on('leave', (object, original) => {
expect(object.get('secretField')).toBeUndefined();
expect(object.get('publicField')).toBe('changed');
expect(original.get('secretField')).toBeUndefined();
expect(original.get('publicField')).toBe('visible');
resolve();
});
}),
obj.save({ publicField: 'changed' }, { useMasterKey: true }),
]);
});
});
+6
View File
@@ -249,6 +249,7 @@ class ParseLiveQueryServer {
if (res.object && typeof res.object.toJSON === 'function') {
deletedParseObject = toJSONwithObjects(res.object, res.object.className || className);
}
res.object = deletedParseObject;
await this._filterSensitiveData(
classLevelPermissions,
res,
@@ -257,6 +258,7 @@ class ParseLiveQueryServer {
op,
subscription.query
);
deletedParseObject = res.object;
client.pushDelete(requestId, deletedParseObject);
} catch (e) {
const error = resolveError(e);
@@ -414,6 +416,8 @@ class ParseLiveQueryServer {
res.original.className || className
);
}
res.object = currentParseObject;
res.original = originalParseObject;
await this._filterSensitiveData(
classLevelPermissions,
res,
@@ -422,6 +426,8 @@ class ParseLiveQueryServer {
op,
subscription.query
);
currentParseObject = res.object;
originalParseObject = res.original ?? null;
const functionName = 'push' + res.event.charAt(0).toUpperCase() + res.event.slice(1);
if (client[functionName]) {
client[functionName](requestId, currentParseObject, originalParseObject);