fix: Denial of service via unindexed database query for unconfigured auth providers ([GHSA-g4cf-xj29-wqqr](https://github.com/parse-community/parse-server/security/advisories/GHSA-g4cf-xj29-wqqr)) (#10271)

This commit is contained in:
Manuel
2026-03-21 15:49:24 +00:00
committed by GitHub
parent e397b83c51
commit 40eb442e02
2 changed files with 71 additions and 1 deletions
+64
View File
@@ -3716,3 +3716,67 @@ describe('(GHSA-6qh5-m6g3-xhq6) LiveQuery query depth DoS via deeply nested subs
expect(subscription).toBeDefined();
});
});
describe('(GHSA-g4cf-xj29-wqqr) DoS via unindexed database query for unconfigured auth providers', () => {
it('should not query database for unconfigured auth provider on signup', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const spy = spyOn(databaseAdapter, 'find').and.callThrough();
await expectAsync(
new Parse.User().save({ authData: { nonExistentProvider: { id: 'test123' } } })
).toBeRejectedWith(
new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE, 'This authentication method is unsupported.')
);
const authDataQueries = spy.calls.all().filter(call => {
const query = call.args[2];
return query?.$or?.some(q => q['authData.nonExistentProvider.id']);
});
expect(authDataQueries.length).toBe(0);
});
it('should not query database for unconfigured auth provider on challenge', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const spy = spyOn(databaseAdapter, 'find').and.callThrough();
await expectAsync(
request({
method: 'POST',
url: Parse.serverURL + '/challenge',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
'Content-Type': 'application/json',
},
body: JSON.stringify({
authData: { nonExistentProvider: { id: 'test123' } },
challengeData: { nonExistentProvider: { token: 'abc' } },
}),
})
).toBeRejected();
const authDataQueries = spy.calls.all().filter(call => {
const query = call.args[2];
return query?.$or?.some(q => q['authData.nonExistentProvider.id']);
});
expect(authDataQueries.length).toBe(0);
});
it('should still query database for configured auth provider', async () => {
await reconfigureServer({
auth: {
myConfiguredProvider: {
module: {
validateAppId: () => Promise.resolve(),
validateAuthData: () => Promise.resolve(),
},
},
},
});
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const spy = spyOn(databaseAdapter, 'find').and.callThrough();
const user = new Parse.User();
await user.save({ authData: { myConfiguredProvider: { id: 'validId', token: 'validToken' } } });
const authDataQueries = spy.calls.all().filter(call => {
const query = call.args[2];
return query?.$or?.some(q => q['authData.myConfiguredProvider.id']);
});
expect(authDataQueries.length).toBeGreaterThan(0);
});
});
+7 -1
View File
@@ -424,7 +424,13 @@ const findUsersWithAuthData = async (config, authData, beforeFind) => {
providers.map(async provider => {
const providerAuthData = authData[provider];
const adapter = config.authDataManager.getValidatorForProvider(provider)?.adapter;
const validatorConfig = config.authDataManager.getValidatorForProvider(provider);
// Skip database query for unconfigured providers to avoid unindexed collection scans;
// the provider will be rejected later in handleAuthDataValidation with UNSUPPORTED_SERVICE
if (!validatorConfig?.validator) {
return null;
}
const adapter = validatorConfig.adapter;
if (beforeFind && typeof adapter?.beforeFind === 'function') {
await adapter.beforeFind(providerAuthData);
}