Description
Angular SSR can retain an incoming streaming request body in memory when it removes an untrusted X-Forwarded-* header.
The affected header-sanitization path creates a new request from request.clone():
return headersDeleted
? new Request(request.clone(), {
signal: request.signal,
headers,
})
: request;
For a POST, PUT, or other request with a body, Request.clone() tees the underlying stream. If the SSR application consumes REQUEST.body, the second unused branch can buffer the uploaded body in Node.js external / ArrayBuffer memory.
As a result, an operation that normally streams with bounded memory can instead consume memory proportional to the request body size.
This affects Angular SSR deployments that:
- use
AngularNodeAppEngine / AngularAppEngine;
- expose a non-
GET SSR route that reads REQUEST.body;
- receive an untrusted
X-Forwarded-* header that Angular removes;
- allow request bodies large enough to create meaningful memory pressure.
Possible examples include SSR form handlers, webhook-style endpoints, upload handlers, PDF/export routes, or custom server-rendered endpoints that use the request body.
The issue is deployment-dependent because reverse proxies or body-size limits may reduce the impact. However, when those limits are permissive, one or more concurrent requests can cause significant process or container memory growth.
Minimal Reproduction
- Create an Angular SSR application using
AngularNodeAppEngine.
- Add a server-rendered route that injects
REQUEST and reads its body as a stream without intentionally buffering it.
- Run the production server with memory logging.
- Send the same large request with and without an untrusted forwarded header.
Control request:
curl -X POST -H "Content-Type: application/octet-stream" --data-binary "@body256.bin" http://localhost:4200/
Triggering request:
curl -X POST -H "Content-Type: application/octet-stream" -H "X-Forwarded-For: 1.2.3.4" --data-binary "@body256.bin" http://localhost:4200/
X-Forwarded-For must not be trusted through trustProxyHeaders, so Angular removes it.
Without the forwarded header, the body is streamed and arrayBuffers returns close to baseline after it is consumed.
With the same body and only the additional untrusted header:
- Angular enters the header-sanitization path;
Request.clone() tees the request body;
- the SSR application consumes one branch;
- the unused branch retains approximately the uploaded body size.
In a 256 MB test, arrayBuffers grew to approximately 260 MB and RSS increased from roughly 143 MB to 378 MB, while the V8 heap remained nearly unchanged.
Concurrent requests multiply the memory impact and may exhaust the SSR worker or container.
POC in https://issuetracker.google.com/u/1/issues/523052797
Your Environment
- Angular CLI / Angular SSR: `22.1.0-next.0`
- Node.js: `22.22.3`
- Build: production
- Engine: `AngularNodeAppEngine` / `AngularAppEngine`
- Render mode: `RenderMode.Server`
Anything else relevant?
This was previusly reported in https://issuetracker.google.com/u/1/issues/523052797
Description
Angular SSR can retain an incoming streaming request body in memory when it removes an untrusted
X-Forwarded-*header.The affected header-sanitization path creates a new request from
request.clone():For a
POST,PUT, or other request with a body,Request.clone()tees the underlying stream. If the SSR application consumesREQUEST.body, the second unused branch can buffer the uploaded body in Node.js external /ArrayBuffermemory.As a result, an operation that normally streams with bounded memory can instead consume memory proportional to the request body size.
This affects Angular SSR deployments that:
AngularNodeAppEngine/AngularAppEngine;GETSSR route that readsREQUEST.body;X-Forwarded-*header that Angular removes;Possible examples include SSR form handlers, webhook-style endpoints, upload handlers, PDF/export routes, or custom server-rendered endpoints that use the request body.
The issue is deployment-dependent because reverse proxies or body-size limits may reduce the impact. However, when those limits are permissive, one or more concurrent requests can cause significant process or container memory growth.
Minimal Reproduction
AngularNodeAppEngine.REQUESTand reads its body as a stream without intentionally buffering it.Control request:
Triggering request:
X-Forwarded-Formust not be trusted throughtrustProxyHeaders, so Angular removes it.Without the forwarded header, the body is streamed and
arrayBuffersreturns close to baseline after it is consumed.With the same body and only the additional untrusted header:
Request.clone()tees the request body;In a 256 MB test,
arrayBuffersgrew to approximately 260 MB and RSS increased from roughly 143 MB to 378 MB, while the V8 heap remained nearly unchanged.Concurrent requests multiply the memory impact and may exhaust the SSR worker or container.
POC in https://issuetracker.google.com/u/1/issues/523052797
Your Environment
Anything else relevant?
This was previusly reported in https://issuetracker.google.com/u/1/issues/523052797