Parse Server: Denial of service via unindexed database query for unconfigured auth providers
Description
Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js. Prior to versions 8.6.58 and 9.6.0-alpha.52, an unauthenticated attacker can cause denial of service by sending authentication requests with arbitrary, unconfigured provider names. The server executes a database query for each unconfigured provider before rejecting the request, and since no database index exists for unconfigured providers, each request triggers a full collection scan on the user database. This can be parallelized to saturate database resources. This issue has been patched in versions 8.6.58 and 9.6.0-alpha.52.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
parse-servernpm | >= 9.0.0, < 9.6.0-alpha.52 | 9.6.0-alpha.52 |
parse-servernpm | < 8.6.58 | 8.6.58 |
Affected products
1- Range: < 8.6.58
Patches
240eb442e0267fix: 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)
2 files changed · +71 −1
spec/vulnerabilities.spec.js+64 −0 modified@@ -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); + }); +});
src/Auth.js+7 −1 modified@@ -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); }
fbac847499e5fix: 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)) (#10270)
2 files changed · +71 −1
spec/vulnerabilities.spec.js+64 −0 modified@@ -4142,3 +4142,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); + }); +});
src/Auth.js+7 −1 modified@@ -448,7 +448,13 @@ const findUsersWithAuthData = async (config, authData, beforeFind, currentUserAu const isUnchanged = storedProviderData && incomingKeys.length > 0 && !incomingKeys.some(key => !isDeepStrictEqual(providerAuthData[key], storedProviderData[key])); - 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' && !isUnchanged) { await adapter.beforeFind(providerAuthData); }
Vulnerability mechanics
Synthesis attempt was rejected by the grounding validator. Re-run pending.
References
7- github.com/advisories/GHSA-g4cf-xj29-wqqrghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-33538ghsaADVISORY
- github.com/parse-community/parse-server/commit/40eb442e02672986730007d0a1edb22c1c4bd357ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/commit/fbac847499e57f243315c5fc7135be1d58bb8e54ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/pull/10270ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/pull/10271ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/security/advisories/GHSA-g4cf-xj29-wqqrghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.