Skip to content

Commit 6009bc1

Browse files
authored
fix: OAuth2 adapter shares mutable state across providers via singleton instance ([GHSA-2cjm-2gwv-m892](GHSA-2cjm-2gwv-m892)) (#10183)
1 parent fe005c3 commit 6009bc1

File tree

2 files changed

+91
-1
lines changed

2 files changed

+91
-1
lines changed

spec/vulnerabilities.spec.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2366,4 +2366,94 @@ describe('(GHSA-c442-97qw-j6c6) SQL Injection via $regex query operator field na
23662366
}
23672367
});
23682368
});
2369+
2370+
describe('(GHSA-2cjm-2gwv-m892) OAuth2 adapter singleton shares mutable state across providers', () => {
2371+
it('should return isolated adapter instances for different OAuth2 providers', () => {
2372+
const { loadAuthAdapter } = require('../lib/Adapters/Auth/index');
2373+
2374+
const authOptions = {
2375+
providerA: {
2376+
oauth2: true,
2377+
tokenIntrospectionEndpointUrl: 'https://a.example.com/introspect',
2378+
useridField: 'sub',
2379+
appidField: 'aud',
2380+
appIds: ['appA'],
2381+
},
2382+
providerB: {
2383+
oauth2: true,
2384+
tokenIntrospectionEndpointUrl: 'https://b.example.com/introspect',
2385+
useridField: 'sub',
2386+
appidField: 'aud',
2387+
appIds: ['appB'],
2388+
},
2389+
};
2390+
2391+
const resultA = loadAuthAdapter('providerA', authOptions);
2392+
const resultB = loadAuthAdapter('providerB', authOptions);
2393+
2394+
// Adapters must be different instances to prevent cross-contamination
2395+
expect(resultA.adapter).not.toBe(resultB.adapter);
2396+
2397+
// After loading providerB, providerA's config must still be intact
2398+
expect(resultA.adapter.tokenIntrospectionEndpointUrl).toBe('https://a.example.com/introspect');
2399+
expect(resultA.adapter.appIds).toEqual(['appA']);
2400+
expect(resultB.adapter.tokenIntrospectionEndpointUrl).toBe('https://b.example.com/introspect');
2401+
expect(resultB.adapter.appIds).toEqual(['appB']);
2402+
});
2403+
2404+
it('should not allow concurrent OAuth2 auth requests to cross-contaminate provider config', async () => {
2405+
await reconfigureServer({
2406+
auth: {
2407+
oauthProviderA: {
2408+
oauth2: true,
2409+
tokenIntrospectionEndpointUrl: 'https://a.example.com/introspect',
2410+
useridField: 'sub',
2411+
appidField: 'aud',
2412+
appIds: ['appA'],
2413+
},
2414+
oauthProviderB: {
2415+
oauth2: true,
2416+
tokenIntrospectionEndpointUrl: 'https://b.example.com/introspect',
2417+
useridField: 'sub',
2418+
appidField: 'aud',
2419+
appIds: ['appB'],
2420+
},
2421+
},
2422+
});
2423+
2424+
// Provider A: valid token with appA audience
2425+
// Provider B: valid token with appB audience
2426+
mockFetch([
2427+
{
2428+
url: 'https://a.example.com/introspect',
2429+
method: 'POST',
2430+
response: {
2431+
ok: true,
2432+
json: () => Promise.resolve({ active: true, sub: 'user1', aud: 'appA' }),
2433+
},
2434+
},
2435+
{
2436+
url: 'https://b.example.com/introspect',
2437+
method: 'POST',
2438+
response: {
2439+
ok: true,
2440+
json: () => Promise.resolve({ active: true, sub: 'user2', aud: 'appB' }),
2441+
},
2442+
},
2443+
]);
2444+
2445+
// Both providers should authenticate independently without cross-contamination
2446+
const [userA, userB] = await Promise.all([
2447+
Parse.User.logInWith('oauthProviderA', {
2448+
authData: { id: 'user1', access_token: 'tokenA' },
2449+
}),
2450+
Parse.User.logInWith('oauthProviderB', {
2451+
authData: { id: 'user2', access_token: 'tokenB' },
2452+
}),
2453+
]);
2454+
2455+
expect(userA.id).toBeDefined();
2456+
expect(userB.id).toBeDefined();
2457+
});
2458+
});
23692459
});

src/Adapters/Auth/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ function loadAuthAdapter(provider, authOptions) {
162162
}
163163

164164
const adapter =
165-
defaultAdapter instanceof AuthAdapter ? defaultAdapter : Object.assign({}, defaultAdapter);
165+
defaultAdapter instanceof AuthAdapter ? new defaultAdapter.constructor() : Object.assign({}, defaultAdapter);
166166
const keys = [
167167
'validateAuthData',
168168
'validateAppId',

0 commit comments

Comments
 (0)