fix: Parse Server OAuth2 authentication adapter account takeover via identity spoofing ([GHSA-fr88-w35c-r596](https://github.com/parse-community/parse-server/security/advisories/GHSA-fr88-w35c-r596)) (#10145)

This commit is contained in:
Manuel
2026-03-08 18:27:06 +00:00
committed by GitHub
parent 6576a19aee
commit 9cfd06e0d0
2 changed files with 109 additions and 2 deletions
+107
View File
@@ -128,6 +128,62 @@ describe('OAuth2Adapter', () => {
adapter.validateAuthData(authData, null, validOptions)
).toBeRejectedWithError('OAuth2 access token is invalid for this user.');
});
it('should default useridField to sub and reject mismatched user ID', async () => {
const adapterNoUseridField = new OAuth2Adapter.constructor();
adapterNoUseridField.validateOptions({
tokenIntrospectionEndpointUrl: 'https://provider.example.com/introspect',
});
const authData = { id: 'victim-user-id', access_token: 'attackerToken' };
const mockResponse = {
active: true,
sub: 'attacker-user-id',
};
mockFetch([
{
url: 'https://provider.example.com/introspect',
method: 'POST',
response: {
ok: true,
json: () => Promise.resolve(mockResponse),
},
},
]);
await expectAsync(
adapterNoUseridField.validateAuthData(authData, null, {})
).toBeRejectedWithError('OAuth2 access token is invalid for this user.');
});
it('should default useridField to sub and accept matching user ID', async () => {
const adapterNoUseridField = new OAuth2Adapter.constructor();
adapterNoUseridField.validateOptions({
tokenIntrospectionEndpointUrl: 'https://provider.example.com/introspect',
});
const authData = { id: 'user-id', access_token: 'validAccessToken' };
const mockResponse = {
active: true,
sub: 'user-id',
};
mockFetch([
{
url: 'https://provider.example.com/introspect',
method: 'POST',
response: {
ok: true,
json: () => Promise.resolve(mockResponse),
},
},
]);
await expectAsync(
adapterNoUseridField.validateAuthData(authData, null, {})
).toBeResolvedTo({});
});
});
describe('requestTokenInfo', () => {
@@ -281,6 +337,57 @@ describe('OAuth2Adapter', () => {
);
});
it('should reject account takeover when useridField is omitted and attacker uses their own token with victim ID', async () => {
await reconfigureServer({
auth: {
mockOauth: {
tokenIntrospectionEndpointUrl: 'https://provider.example.com/introspect',
authorizationHeader: 'Bearer validAuthToken',
oauth2: true,
},
},
});
// Victim signs up with their own valid token
mockFetch([
{
url: 'https://provider.example.com/introspect',
method: 'POST',
response: {
ok: true,
json: () => Promise.resolve({
active: true,
sub: 'victim-sub-id',
}),
},
},
]);
const victimAuthData = { access_token: 'victimToken', id: 'victim-sub-id' };
const victim = await Parse.User.logInWith('mockOauth', { authData: victimAuthData });
expect(victim.id).toBeDefined();
// Attacker tries to log in with their own valid token but claims victim's ID
mockFetch([
{
url: 'https://provider.example.com/introspect',
method: 'POST',
response: {
ok: true,
json: () => Promise.resolve({
active: true,
sub: 'attacker-sub-id',
}),
},
},
]);
const attackerAuthData = { access_token: 'attackerToken', id: 'victim-sub-id' };
await expectAsync(Parse.User.logInWith('mockOauth', { authData: attackerAuthData })).toBeRejectedWith(
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.')
);
});
it('should handle error when token introspection endpoint is missing', async () => {
await reconfigureServer({
auth: {
+2 -2
View File
@@ -5,7 +5,7 @@
* @param {Object} options - The adapter configuration options.
* @param {string} options.tokenIntrospectionEndpointUrl - The URL of the token introspection endpoint. Required.
* @param {boolean} options.oauth2 - Indicates that the request should be handled by the OAuth2 adapter. Required.
* @param {string} [options.useridField] - The field in the introspection response that contains the user ID. Optional.
* @param {string} [options.useridField='sub'] - The field in the introspection response that contains the user ID. Defaults to `sub` per RFC 7662.
* @param {string} [options.appidField] - The field in the introspection response that contains the app ID. Optional.
* @param {string[]} [options.appIds] - List of allowed app IDs. Required if `appidField` is defined.
* @param {string} [options.authorizationHeader] - The Authorization header value for the introspection request. Optional.
@@ -66,7 +66,7 @@ class OAuth2Adapter extends AuthAdapter {
}
this.tokenIntrospectionEndpointUrl = options.tokenIntrospectionEndpointUrl;
this.useridField = options.useridField;
this.useridField = options.useridField || 'sub';
this.appidField = options.appidField;
this.appIds = options.appIds;
this.authorizationHeader = options.authorizationHeader;