diff --git a/.changeset/large-cats-reply.md b/.changeset/large-cats-reply.md new file mode 100644 index 0000000000..0e5b3696e7 --- /dev/null +++ b/.changeset/large-cats-reply.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Limit the size of fetched client ID metadata documents to prevent oversized responses from being accepted. diff --git a/plugins/auth-backend/src/service/CimdClient.test.ts b/plugins/auth-backend/src/service/CimdClient.test.ts index 03d453e5e9..8c21ec3167 100644 --- a/plugins/auth-backend/src/service/CimdClient.test.ts +++ b/plugins/auth-backend/src/service/CimdClient.test.ts @@ -328,6 +328,29 @@ describe('CimdClient', () => { ).rejects.toThrow('Invalid client metadata document'); }); + it('should throw for oversized JSON without content-length', async () => { + const oversizedMetadata = { + client_id: 'https://example.com/oauth-metadata.json', + client_name: 'x'.repeat(64 * 1024), + redirect_uris: ['http://localhost:8080/callback'], + }; + const fetchMock = jest.spyOn(global, 'fetch').mockResolvedValue( + new Response(JSON.stringify(oversizedMetadata), { + headers: { 'content-type': 'application/json' }, + }), + ); + + try { + await expect( + fetchCimdMetadata({ + clientId: 'https://example.com/oauth-metadata.json', + }), + ).rejects.toThrow('Client metadata document too large'); + } finally { + fetchMock.mockRestore(); + } + }); + it('should throw for client_id mismatch', async () => { const mismatchedMetadata = { client_id: 'https://different.com/metadata', diff --git a/plugins/auth-backend/src/service/CimdClient.ts b/plugins/auth-backend/src/service/CimdClient.ts index 7d8567f97d..eee9b9637f 100644 --- a/plugins/auth-backend/src/service/CimdClient.ts +++ b/plugins/auth-backend/src/service/CimdClient.ts @@ -187,6 +187,24 @@ function validateMetadata( } } +async function readCappedResponseBody(response: Response): Promise { + if (!response.body) { + return ''; + } + + const chunks: Buffer[] = []; + let received = 0; + for await (const chunk of response.body) { + received += chunk.byteLength; + if (received > MAX_RESPONSE_BYTES) { + throw new InputError('Client metadata document too large'); + } + chunks.push(Buffer.from(chunk)); + } + + return Buffer.concat(chunks).toString('utf8'); +} + /** * Fetches and validates a CIMD metadata document. * @throws InputError if fetching or validation fails @@ -228,8 +246,12 @@ export async function fetchCimdMetadata(opts: { let metadata: CimdMetadata; try { - metadata = await response.json(); - } catch { + const responseBody = await readCappedResponseBody(response); + metadata = JSON.parse(responseBody) as CimdMetadata; + } catch (error) { + if (isError(error) && error.name === 'InputError') { + throw error; + } throw new InputError('Invalid client metadata document'); }