Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
96 changes: 96 additions & 0 deletions spec/vulnerabilities.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1706,6 +1706,102 @@ describe('(GHSA-r2m8-pxm9-9c4g) Protected fields WHERE clause bypass via dot-not
});
});

describe('(GHSA-j7mm-f4rv-6q6q) Protected fields bypass via LiveQuery dot-notation WHERE', () => {
let obj;

beforeEach(async () => {
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
await reconfigureServer({
liveQuery: { classNames: ['SecretClass'] },
startLiveQueryServer: true,
verbose: false,
silent: true,
});
const config = Config.get(Parse.applicationId);
const schemaController = await config.database.loadSchema();
await schemaController.addClassIfNotExists(
'SecretClass',
{ secretObj: { type: 'Object' }, publicField: { type: 'String' } },
);
await schemaController.updateClass(
'SecretClass',
{},
{
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: {},
protectedFields: { '*': ['secretObj'] },
}
);

obj = new Parse.Object('SecretClass');
obj.set('secretObj', { apiKey: 'SENSITIVE_KEY_123', score: 42 });
obj.set('publicField', 'visible');
await obj.save(null, { useMasterKey: true });
});

afterEach(async () => {
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
await client.close();
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it('should reject LiveQuery subscription with dot-notation on protected field in where clause', async () => {
const query = new Parse.Query('SecretClass');
query._addCondition('secretObj.apiKey', '$eq', 'SENSITIVE_KEY_123');
await expectAsync(query.subscribe()).toBeRejected();
});

it('should reject LiveQuery subscription with protected field directly in where clause', async () => {
const query = new Parse.Query('SecretClass');
query.exists('secretObj');
await expectAsync(query.subscribe()).toBeRejected();
});

it('should reject LiveQuery subscription with protected field in $or', async () => {
const q1 = new Parse.Query('SecretClass');
q1._addCondition('secretObj.apiKey', '$eq', 'SENSITIVE_KEY_123');
const q2 = new Parse.Query('SecretClass');
q2._addCondition('secretObj.apiKey', '$eq', 'other');
const query = Parse.Query.or(q1, q2);
await expectAsync(query.subscribe()).toBeRejected();
});

it('should reject LiveQuery subscription with $regex on protected field (boolean oracle)', async () => {
const query = new Parse.Query('SecretClass');
query._addCondition('secretObj.apiKey', '$regex', '^S');
await expectAsync(query.subscribe()).toBeRejected();
});

it('should reject LiveQuery subscription with deeply nested dot-notation on protected field', async () => {
const query = new Parse.Query('SecretClass');
query._addCondition('secretObj.nested.deep.key', '$eq', 'value');
await expectAsync(query.subscribe()).toBeRejected();
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it('should allow LiveQuery subscription on non-protected fields and strip protected fields from response', async () => {
const query = new Parse.Query('SecretClass');
query.exists('publicField');
const subscription = await query.subscribe();
await Promise.all([
new Promise(resolve => {
subscription.on('update', object => {
expect(object.get('secretObj')).toBeUndefined();
expect(object.get('publicField')).toBe('updated');
resolve();
});
}),
obj.save({ publicField: 'updated' }, { useMasterKey: true }),
]);
});

// Note: master key bypass is inherently tested by the `!client.hasMasterKey` guard
// in the implementation. Testing master key LiveQuery requires configuring keyPairs
// in the LiveQuery server config, which is not part of the default test setup.
});

describe('(GHSA-w54v-hf9p-8856) User enumeration via email verification endpoint', () => {
let sendVerificationEmail;

Expand Down
35 changes: 35 additions & 0 deletions src/LiveQuery/ParseLiveQueryServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,41 @@ class ParseLiveQueryServer {
op
);

// Check protected fields in WHERE clause
if (!client.hasMasterKey) {
const auth = request.user ? { user: request.user, userRoles: [] } : {};
const protectedFields =
appConfig.database.addProtectedFields(
classLevelPermissions,
className,
request.query.where,
aclGroup,
auth
) || [];
if (protectedFields.length > 0 && request.query.where) {
const checkWhere = (where: any) => {
if (typeof where !== 'object' || where === null) {
return;
}
for (const whereKey of Object.keys(where)) {
const rootField = whereKey.split('.')[0];
if (protectedFields.includes(whereKey) || protectedFields.includes(rootField)) {
throw new Parse.Error(
Parse.Error.OPERATION_FORBIDDEN,
'Permission denied'
);
}
}
for (const op of ['$or', '$and', '$nor']) {
if (Array.isArray(where[op])) {
where[op].forEach((subQuery: any) => checkWhere(subQuery));
}
}
};
checkWhere(request.query.where);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Get subscription from subscriptions, create one if necessary
const subscriptionHash = queryHash(request.query);
// Add className to subscriptions if necessary
Expand Down
Loading