Skip to content

Angular SSR buffers streaming request bodies when sanitizing untrusted X-Forwarded-* headers #33706

Description

@SkyZeroZx

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

  1. Create an Angular SSR application using AngularNodeAppEngine.
  2. Add a server-rendered route that injects REQUEST and reads its body as a stream without intentionally buffering it.
  3. Run the production server with memory logging.
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions