Merge commit from fork
* Fix redirect URI allowlist bypass via URL userinfo syntax * Validate redirect URIs against normalized origin+pathname
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Improved redirect URI validation in the experimental OIDC provider to match against normalized URLs rather than raw strings.
|
||||
@@ -283,6 +283,33 @@ describe('OidcService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject redirect URIs containing userinfo', async () => {
|
||||
const { service } = await createOidcService({
|
||||
databaseId,
|
||||
config: {
|
||||
auth: {
|
||||
experimentalDynamicClientRegistration: {
|
||||
allowedRedirectUriPatterns: ['http://localhost:*'],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.registerClient({
|
||||
clientName: 'Evil Client',
|
||||
redirectUris: ['http://localhost:3000@attacker.example/callback'],
|
||||
}),
|
||||
).rejects.toThrow('Invalid redirect_uri');
|
||||
|
||||
await expect(
|
||||
service.registerClient({
|
||||
clientName: 'Evil Client',
|
||||
redirectUris: ['http://user:pass@example.com/callback'],
|
||||
}),
|
||||
).rejects.toThrow('Invalid redirect_uri');
|
||||
});
|
||||
|
||||
it('should create a client with default values', async () => {
|
||||
const { service } = await createOidcService({ databaseId });
|
||||
|
||||
|
||||
@@ -29,6 +29,18 @@ import matcher from 'matcher';
|
||||
import { OfflineAccessService } from './OfflineAccessService';
|
||||
import { validateCimdUrl, fetchCimdMetadata } from './CimdClient';
|
||||
|
||||
function validateRedirectUri(
|
||||
redirectUri: string,
|
||||
allowedPatterns: string[],
|
||||
): void {
|
||||
const parsed = new URL(redirectUri);
|
||||
const normalized = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
||||
|
||||
if (!allowedPatterns.some(pattern => matcher.isMatch(normalized, pattern))) {
|
||||
throw new InputError('Invalid redirect_uri');
|
||||
}
|
||||
}
|
||||
|
||||
export class OidcService {
|
||||
private readonly auth: AuthService;
|
||||
private readonly tokenIssuer: TokenIssuer;
|
||||
@@ -162,13 +174,7 @@ export class OidcService {
|
||||
) ?? ['*'];
|
||||
|
||||
for (const redirectUri of opts.redirectUris ?? []) {
|
||||
if (
|
||||
!allowedRedirectUriPatterns.some(pattern =>
|
||||
matcher.isMatch(redirectUri, pattern),
|
||||
)
|
||||
) {
|
||||
throw new InputError('Invalid redirect_uri');
|
||||
}
|
||||
validateRedirectUri(redirectUri, allowedRedirectUriPatterns);
|
||||
}
|
||||
|
||||
return await this.oidc.createClient({
|
||||
@@ -305,13 +311,7 @@ export class OidcService {
|
||||
});
|
||||
|
||||
if (opts.redirectUri) {
|
||||
if (
|
||||
!cimd.allowedRedirectUriPatterns.some(pattern =>
|
||||
matcher.isMatch(opts.redirectUri!, pattern),
|
||||
)
|
||||
) {
|
||||
throw new InputError('Invalid redirect_uri');
|
||||
}
|
||||
validateRedirectUri(opts.redirectUri, cimd.allowedRedirectUriPatterns);
|
||||
|
||||
if (!cimdClient.redirectUris.includes(opts.redirectUri)) {
|
||||
throw new InputError('Redirect URI not registered');
|
||||
|
||||
Reference in New Issue
Block a user