Skip to content

Commit 61179a4

Browse files
committed
fix
1 parent 0744225 commit 61179a4

File tree

2 files changed

+131
-0
lines changed

2 files changed

+131
-0
lines changed

spec/vulnerabilities.spec.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1706,6 +1706,102 @@ describe('(GHSA-r2m8-pxm9-9c4g) Protected fields WHERE clause bypass via dot-not
17061706
});
17071707
});
17081708

1709+
describe('(GHSA-j7mm-f4rv-6q6q) Protected fields bypass via LiveQuery dot-notation WHERE', () => {
1710+
let obj;
1711+
1712+
beforeEach(async () => {
1713+
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
1714+
await reconfigureServer({
1715+
liveQuery: { classNames: ['SecretClass'] },
1716+
startLiveQueryServer: true,
1717+
verbose: false,
1718+
silent: true,
1719+
});
1720+
const config = Config.get(Parse.applicationId);
1721+
const schemaController = await config.database.loadSchema();
1722+
await schemaController.addClassIfNotExists(
1723+
'SecretClass',
1724+
{ secretObj: { type: 'Object' }, publicField: { type: 'String' } },
1725+
);
1726+
await schemaController.updateClass(
1727+
'SecretClass',
1728+
{},
1729+
{
1730+
find: { '*': true },
1731+
get: { '*': true },
1732+
create: { '*': true },
1733+
update: { '*': true },
1734+
delete: { '*': true },
1735+
addField: {},
1736+
protectedFields: { '*': ['secretObj'] },
1737+
}
1738+
);
1739+
1740+
obj = new Parse.Object('SecretClass');
1741+
obj.set('secretObj', { apiKey: 'SENSITIVE_KEY_123', score: 42 });
1742+
obj.set('publicField', 'visible');
1743+
await obj.save(null, { useMasterKey: true });
1744+
});
1745+
1746+
afterEach(async () => {
1747+
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
1748+
await client.close();
1749+
});
1750+
1751+
it('should reject LiveQuery subscription with dot-notation on protected field in where clause', async () => {
1752+
const query = new Parse.Query('SecretClass');
1753+
query._addCondition('secretObj.apiKey', '$eq', 'SENSITIVE_KEY_123');
1754+
await expectAsync(query.subscribe()).toBeRejected();
1755+
});
1756+
1757+
it('should reject LiveQuery subscription with protected field directly in where clause', async () => {
1758+
const query = new Parse.Query('SecretClass');
1759+
query.exists('secretObj');
1760+
await expectAsync(query.subscribe()).toBeRejected();
1761+
});
1762+
1763+
it('should reject LiveQuery subscription with protected field in $or', async () => {
1764+
const q1 = new Parse.Query('SecretClass');
1765+
q1._addCondition('secretObj.apiKey', '$eq', 'SENSITIVE_KEY_123');
1766+
const q2 = new Parse.Query('SecretClass');
1767+
q2._addCondition('secretObj.apiKey', '$eq', 'other');
1768+
const query = Parse.Query.or(q1, q2);
1769+
await expectAsync(query.subscribe()).toBeRejected();
1770+
});
1771+
1772+
it('should reject LiveQuery subscription with $regex on protected field (boolean oracle)', async () => {
1773+
const query = new Parse.Query('SecretClass');
1774+
query._addCondition('secretObj.apiKey', '$regex', '^S');
1775+
await expectAsync(query.subscribe()).toBeRejected();
1776+
});
1777+
1778+
it('should reject LiveQuery subscription with deeply nested dot-notation on protected field', async () => {
1779+
const query = new Parse.Query('SecretClass');
1780+
query._addCondition('secretObj.nested.deep.key', '$eq', 'value');
1781+
await expectAsync(query.subscribe()).toBeRejected();
1782+
});
1783+
1784+
it('should allow LiveQuery subscription on non-protected fields and strip protected fields from response', async () => {
1785+
const query = new Parse.Query('SecretClass');
1786+
query.exists('publicField');
1787+
const subscription = await query.subscribe();
1788+
await Promise.all([
1789+
new Promise(resolve => {
1790+
subscription.on('update', object => {
1791+
expect(object.get('secretObj')).toBeUndefined();
1792+
expect(object.get('publicField')).toBe('updated');
1793+
resolve();
1794+
});
1795+
}),
1796+
obj.save({ publicField: 'updated' }, { useMasterKey: true }),
1797+
]);
1798+
});
1799+
1800+
// Note: master key bypass is inherently tested by the `!client.hasMasterKey` guard
1801+
// in the implementation. Testing master key LiveQuery requires configuring keyPairs
1802+
// in the LiveQuery server config, which is not part of the default test setup.
1803+
});
1804+
17091805
describe('(GHSA-w54v-hf9p-8856) User enumeration via email verification endpoint', () => {
17101806
let sendVerificationEmail;
17111807

src/LiveQuery/ParseLiveQueryServer.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -916,6 +916,41 @@ class ParseLiveQueryServer {
916916
op
917917
);
918918

919+
// Check protected fields in WHERE clause
920+
if (!client.hasMasterKey) {
921+
const auth = request.user ? { user: request.user, userRoles: [] } : {};
922+
const protectedFields =
923+
appConfig.database.addProtectedFields(
924+
classLevelPermissions,
925+
className,
926+
request.query.where,
927+
aclGroup,
928+
auth
929+
) || [];
930+
if (protectedFields.length > 0 && request.query.where) {
931+
const checkWhere = (where: any) => {
932+
if (typeof where !== 'object' || where === null) {
933+
return;
934+
}
935+
for (const whereKey of Object.keys(where)) {
936+
const rootField = whereKey.split('.')[0];
937+
if (protectedFields.includes(whereKey) || protectedFields.includes(rootField)) {
938+
throw new Parse.Error(
939+
Parse.Error.OPERATION_FORBIDDEN,
940+
'Permission denied'
941+
);
942+
}
943+
}
944+
for (const op of ['$or', '$and', '$nor']) {
945+
if (Array.isArray(where[op])) {
946+
where[op].forEach((subQuery: any) => checkWhere(subQuery));
947+
}
948+
}
949+
};
950+
checkWhere(request.query.where);
951+
}
952+
}
953+
919954
// Get subscription from subscriptions, create one if necessary
920955
const subscriptionHash = queryHash(request.query);
921956
// Add className to subscriptions if necessary

0 commit comments

Comments
 (0)