mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
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:
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user