From 8eaf213e3dbcf5f7b5840e540bbec62952bb9f77 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Fri, 31 Jul 2026 06:58:00 +0000 Subject: [PATCH 1/2] perf(@angular/ssr): avoid buffering request body when sanitizing headers Avoid teeing the incoming request body stream with `request.clone()` when removing untrusted `X-Forwarded-*` headers in `sanitizeRequestHeaders`. Teeing the stream caused the unconsumed branch to buffer the uploaded body in memory. Passing `request` directly to `new Request(request, { headers })` transfers the stream without teeing or buffering. Additionally, `request.signal` is automatically inherited by the new `Request` instance without needing to pass it explicitly. Closes #33706 --- packages/angular/ssr/src/utils/validation.ts | 5 ++--- .../angular/ssr/test/utils/validation_spec.ts | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/angular/ssr/src/utils/validation.ts b/packages/angular/ssr/src/utils/validation.ts index 714042b52f76..73e6c712d80b 100644 --- a/packages/angular/ssr/src/utils/validation.ts +++ b/packages/angular/ssr/src/utils/validation.ts @@ -88,7 +88,7 @@ export function validateUrl(url: URL, allowedHosts: ReadonlySet): void { /** * Sanitizes the proxy headers of a request by removing unallowed `X-Forwarded-*` headers. - * If no headers need to be removed, the original request is returned without cloning. + * If no headers need to be removed, the original request is returned unchanged. * * @param request - The incoming `Request` object to sanitize. * @param trustProxyHeaders - A set of allowed proxy headers. @@ -117,8 +117,7 @@ export function sanitizeRequestHeaders( } return headersDeleted - ? new Request(request.clone(), { - signal: request.signal, + ? new Request(request, { headers, }) : request; diff --git a/packages/angular/ssr/test/utils/validation_spec.ts b/packages/angular/ssr/test/utils/validation_spec.ts index 0f44f8067aeb..753dcf3cf1de 100644 --- a/packages/angular/ssr/test/utils/validation_spec.ts +++ b/packages/angular/ssr/test/utils/validation_spec.ts @@ -493,5 +493,26 @@ describe('Validation Utils', () => { expect(secured.headers.get('host')).toBe('example.com'); expect(secured.headers.get('forwarded')).toBe('host=proxy.com;proto=https'); }); + + it('should not tee request body and should preserve abort signal when removing unallowed headers', async () => { + const controller = new AbortController(); + const req = new Request('http://example.com', { + method: 'POST', + body: 'test body', + signal: controller.signal, + headers: { + 'host': 'example.com', + 'x-forwarded-host': 'evil.com', + }, + }); + + const secured = sanitizeRequestHeaders(req, normalizeTrustProxyHeaders(undefined)); + + expect(req.bodyUsed).toBeTrue(); + expect(await secured.text()).toBe('test body'); + + controller.abort(); + expect(secured.signal.aborted).toBeTrue(); + }); }); }); From c18eef3a159183f58cd7486c9c49ca6e7785e292 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Fri, 31 Jul 2026 17:38:25 +0000 Subject: [PATCH 2/2] fixup! perf(@angular/ssr): avoid buffering request body when sanitizing headers --- .../angular/ssr/test/utils/validation_spec.ts | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/angular/ssr/test/utils/validation_spec.ts b/packages/angular/ssr/test/utils/validation_spec.ts index 753dcf3cf1de..c9986a9bd73a 100644 --- a/packages/angular/ssr/test/utils/validation_spec.ts +++ b/packages/angular/ssr/test/utils/validation_spec.ts @@ -494,12 +494,10 @@ describe('Validation Utils', () => { expect(secured.headers.get('forwarded')).toBe('host=proxy.com;proto=https'); }); - it('should not tee request body and should preserve abort signal when removing unallowed headers', async () => { - const controller = new AbortController(); + it('should transfer request body without teeing when removing unallowed headers', async () => { const req = new Request('http://example.com', { method: 'POST', body: 'test body', - signal: controller.signal, headers: { 'host': 'example.com', 'x-forwarded-host': 'evil.com', @@ -508,8 +506,24 @@ describe('Validation Utils', () => { const secured = sanitizeRequestHeaders(req, normalizeTrustProxyHeaders(undefined)); + // In the Fetch specification, calling `request.clone()` tees the body stream and leaves + // `req.bodyUsed` as `false`. Passing `req` directly to `new Request(req, ...)` transfers the + // underlying stream without teeing, immediately marking `req.bodyUsed` as `true`. expect(req.bodyUsed).toBeTrue(); expect(await secured.text()).toBe('test body'); + }); + + it('should preserve abort signal when removing unallowed headers', () => { + const controller = new AbortController(); + const req = new Request('http://example.com', { + signal: controller.signal, + headers: { + 'host': 'example.com', + 'x-forwarded-host': 'evil.com', + }, + }); + + const secured = sanitizeRequestHeaders(req, normalizeTrustProxyHeaders(undefined)); controller.abort(); expect(secured.signal.aborted).toBeTrue();