Client IP lost from X-Forwarded-For after upgrading Tanzu Spring API Gateway to 2.4.5 or later
search cancel

Client IP lost from X-Forwarded-For after upgrading Tanzu Spring API Gateway to 2.4.5 or later

book

Article ID: 452962

calendar_today

Updated On:

Products

VMware Tanzu Platform - Hub

Issue/Introduction

After upgrading from API Gateway 2.3.x to 2.4.5/2.4.6, the X-Forwarded-For header received by applications behind the Gateway no longer contains the original client IP — only a single, incorrect address (typically the immediate upstream peer, e.g. a router/NAT IP) is present.

 

Before (2.3.x):

x_forwarded_for:"<CLIENT_IP>, <NAT_IP>,<CLIENT_IP>, <NAT_IP>"

After (2.4.6):

x_forwarded_for:"<NAT_IP>"

The real client IP is dropped entirely; only the NAT/LB IP of the immediate upstream hop remains.

Environment

  • Tanzu Spring API Gateway 2.4.x and later (Spring Cloud Gateway Server WebFlux 4.3.x / Spring Cloud 2025.0.x)
  • Applies to any topology where API Gateway sits behind one or more upstream proxies/routers (e.g. Router → frontend → Gateway → backend app)

Cause

This is a known, unintended side effect of the fix for CVE-2025-41235 ("Spring Cloud Gateway Server Forwards Headers from Untrusted Proxies"), which landed in API Gateway 2.3.3 and was tightened further in 2.4.5 (see commit 9041b549).

Before the fix, X-Forwarded-For/Forwarded headers from any upstream hop were trusted and forwarded unconditionally. The fix makes this secure-by-default: headers are only trusted/forwarded when the connecting peer matches a configured spring.cloud.gateway.server.webflux.trusted-proxies regex, and requires spring.cloud.gateway.server.webflux.httpserver.customizer-enabled=true to even enable this evaluation.

However, the fix introduced a conflict between two independent trust checks in the request pipeline:

  1. DefaultNettyHttpForwardedHeaderHandler (gated by httpserver.customizer-enabled + trusted-proxies) validates the direct TCP peer, and on success replaces request.getRemoteAddress() with the client IP parsed out of the inbound X-Forwarded-For header.
  2. XForwardedHeadersFilter then performs its own separate trust check — but by this point request.getRemoteAddress() has already been overwritten to the client IP, not the proxy IP that was actually validated. This second check typically fails (or produces duplicated/incorrect values), causing the original forwarded chain to be discarded.

This is documented in the upstream thread spring-cloud-gateway#3818, including a community root-cause writeup confirming the conflict between the Netty customizer and the header filter.

Resolution

There is no supported configuration-only fix. Any config change permissive enough to restore the full forwarded chain (e.g. trusted-proxies=.* or trusted-proxies=[\s\S]*) re-widens the trust check to match anything, which reintroduces the same untrusted-header-forwarding exposure that CVE-2025-41235 was fixed to close — and even then, may only produce duplicated/malformed values rather than a fully correct chain, since the underlying conflict between the two trust checks isn't actually resolved by widening the regex.

This will be fixed in the upcoming releases.