Skip to content

SSR strict host validation breaks application on cloud platform #32616

@julianclem

Description

@julianclem

Command

other

Is this a regression?

  • Yes, this behavior used to work in the previous version

The previous version in which this bug was not present was

21.1.4

Description

My SSR application runs in a Kubernetes cluster, and behind a load balancer. After updating @angular/ssr to version 21.1.5, which contains the SSRF fix #32516, I am getting the following error logged server-side for every request:

URL with hostname "10.x.x.x" is not allowed.Please provide a list of allowed hosts in the "allowedHosts" option in the "CommonEngine" constructor.
Falling back to client side rendering. This will become a 400 Bad Request in a future major version.

The address 10.x.x.x varies from one request to the next, because Kubernetes spins up multiple containers, each with a different private address.

I assume that either the load balancer or Kubernetes itself (not sure which) is passing the real hostname as x-forwarded-host: my.real.hostname, while setting host: 10.x.x.x on the internal request, where 10.x.x.x is the dynamic internal IP request of each container instance.

I cannot resolve this with the allowedHosts option (or the NG_ALLOWED_HOSTS environment variable), because this option only supports either exact hostnames or a wildcard prefix such as *.domain. It does not support a wildcard postfix such as 10.*. I can't even hack a fix by using just *, as this also fails to match.

I can think of two possible solutions, each very different:

  1. Extend the allowedHosts option to support glob-style wildcards. Then I could set allowedHosts to ['10.*', 'my.real.hostname'] (allowing for both headers).
  2. If the later code that this validation is protecting always derives the real hostname as x-forwarded-host || host (pseudocode), then this SSRF protection should only need to validate the host header if x-forwarded-host is not present. Then I could set allowedHosts to just ['my.real.hostname'] and the private IP address in host wouldn't matter.

Minimal Reproduction

  • Create a new application with the latest versions, with @angular/ssr at version 21.1.5 or 21.2.0, and SSR enabled.
  • Add at least one RenderMode.Server page in the server routes.
  • Running the compiled module locally with Node.js, verify that the URL with hostname "localhost" is not allowed error occurs, but can be resolved by setting NG_ALLOWED_HOSTS=localhost, and that the SSR route is now server-side rendered.
  • Build the compiled app into a Node.js container image.
  • Deploy the container to a cloud service and give it a public hostname, but in such a way that traffic will be routed internally using a private IP address as the host header. (It may also be possible to reproduce running a Kubernetes cluster locally without a load balancer. I haven't verified this.)
  • Access the SSR route, and observe that the page is not server-side rendered, and the URL with hostname "10.x.x.x" is not allowed error (or equivalent IP address) is logged out by the container.
  • Try setting the NG_ALLOWED_HOSTS environment variable on the container, and observe that it is not possible to find a value that successfully resolves the error, except supplying the public hostname and the exact private IP address – until it changes. Any other value you try using a wildcard results in a 400 error.

I can try to create a simple example repo if necessary, but I hope this explanation is enough, together with inspecting the code change introduced in #32516.

Exception or Error

URL with hostname "10.x.x.x" is not allowed.Please provide a list of allowed hosts in the "allowedHosts" option in the "CommonEngine" constructor.
Falling back to client side rendering. This will become a 400 Bad Request in a future major version.

Your Environment

┌───────────────────────────────────┬───────────────────┬───────────────────┐
│ Package                           │ Installed Version │ Requested Version │
├───────────────────────────────────┼───────────────────┼───────────────────┤
│ @angular-devkit/build-angular     │ 21.1.5            │ 21.1.5            │
│ @angular-devkit/core              │ 21.1.5            │ 21.1.5            │
│ @angular-devkit/schematics        │ 21.1.5            │ 21.1.5            │
│ @angular/animations               │ 21.1.6            │ 21.1.6            │
│ @angular/build                    │ 21.1.5            │ 21.1.5            │
│ @angular/cdk                      │ 21.1.6            │ 21.1.6            │
│ @angular/cli                      │ 21.1.5            │ 21.1.5            │
│ @angular/common                   │ 21.1.6            │ 21.1.6            │
│ @angular/compiler                 │ 21.1.6            │ 21.1.6            │
│ @angular/compiler-cli             │ 21.1.6            │ 21.1.6            │
│ @angular/core                     │ 21.1.6            │ 21.1.6            │
│ @angular/forms                    │ 21.1.6            │ 21.1.6            │
│ @angular/language-service         │ 21.1.6            │ 21.1.6            │
│ @angular/localize                 │ 21.1.6            │ 21.1.6            │
│ @angular/material                 │ 21.1.6            │ 21.1.6            │
│ @angular/platform-browser         │ 21.1.6            │ 21.1.6            │
│ @angular/platform-browser-dynamic │ 21.1.6            │ 21.1.6            │
│ @angular/platform-server          │ 21.1.6            │ 21.1.6            │
│ @angular/router                   │ 21.1.6            │ 21.1.6            │
│ @angular/ssr                      │ 21.1.5            │ 21.1.5            │
│ @schematics/angular               │ 21.1.5            │ 21.1.5            │
│ rxjs                              │ 7.8.2             │ 7.8.2             │
│ typescript                        │ 5.9.3             │ 5.9.3             │
│ zone.js                           │ 0.16.0            │ 0.16.0            │
└───────────────────────────────────┴───────────────────┴───────────────────┘

Anything else relevant?

I am using Google Kubernetes Engine behind Google Load Balancer, but other platforms may also show the issue. (I'm not certain whether it's the Load Balancer or Kubernetes that is using the IP address as the host header; possibly the latter, in which case it might be possible to reproduce the issue running Kubernetes locally.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions