diff --git a/.changeset/fix-cimd-ssrf-redirect-bypass.md b/.changeset/fix-cimd-ssrf-redirect-bypass.md new file mode 100644 index 0000000000..29e70887b4 --- /dev/null +++ b/.changeset/fix-cimd-ssrf-redirect-bypass.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Fixed a security vulnerability where the CIMD metadata fetch could follow HTTP redirects to internal hosts, bypassing SSRF protections. diff --git a/plugins/auth-backend/src/service/CimdClient.test.ts b/plugins/auth-backend/src/service/CimdClient.test.ts index b61a9c88cf..4aad136db8 100644 --- a/plugins/auth-backend/src/service/CimdClient.test.ts +++ b/plugins/auth-backend/src/service/CimdClient.test.ts @@ -300,6 +300,42 @@ describe('CimdClient', () => { }); }); + describe('redirect protection', () => { + it('should reject redirects to prevent SSRF via redirect bypass', async () => { + const redirectTarget = jest.fn(); + + server.use( + rest.get( + 'https://example.com/oauth-metadata.json', + (_req, res, ctx) => { + return res( + ctx.status(302), + ctx.set('Location', 'http://127.0.0.1:8080/internal'), + ); + }, + ), + rest.get('http://127.0.0.1:8080/internal', (req, res, ctx) => { + redirectTarget(); + return res( + ctx.json({ + client_id: 'https://example.com/oauth-metadata.json', + client_name: 'Sneaky Client', + redirect_uris: ['http://localhost:8080/callback'], + }), + ); + }), + ); + + await expect( + fetchCimdMetadata({ + clientId: 'https://example.com/oauth-metadata.json', + }), + ).rejects.toThrow('Failed to fetch client metadata'); + + expect(redirectTarget).not.toHaveBeenCalled(); + }); + }); + describe('HTTP error handling', () => { it('should throw for network errors', async () => { server.use( diff --git a/plugins/auth-backend/src/service/CimdClient.ts b/plugins/auth-backend/src/service/CimdClient.ts index c610de3708..9520517f25 100644 --- a/plugins/auth-backend/src/service/CimdClient.ts +++ b/plugins/auth-backend/src/service/CimdClient.ts @@ -224,6 +224,7 @@ export async function fetchCimdMetadata(opts: { method: 'GET', headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), + redirect: 'error', }); } catch { throw new InputError('Failed to fetch client metadata');