spring-cloud / spring-cloud/spring-cloud-gateway

ForwardedHeadersFilter drops upstream Forwarded headers without a `for` field

Open
#4,214 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

waiting-for-triage
Dominant language
Java
Stars
4.9k
Forks
3.5k
Avg merge
20h 57m
Merged PRs (30d)
8

Description

(Please note that this bug report has been authored partially with the help of AI)

Summary

The ForwardedHeadersFilter removes upstream Forwarded headers if they do not contain a for field, even if they have been added by a trusted proxy.

Environment

  • Component: spring-cloud-gateway-server-webflux
  • Affected class: org.springframework.cloud.gateway.filter.headers.ForwardedHeadersFilter
  • Verified against version: 5.0.2

Steps to Reproduce

Set up a two-proxy chain:

Client → Upstream Proxy → Spring Cloud Gateway → Downstream Service
  1. The client sends a request with Host: upstream.example.com to the upstream proxy.
  2. The upstream proxy adds a Forwarded: host=upstream.example.com;proto=https entry (no for field — this is valid per RFC 7239, Section 5.2, where all parameters are optional) and forwards the request to the gateway with Host: intermediate.example.com.
  3. Spring Cloud Gateway's ForwardedHeadersFilter processes the request.
  4. Observe the Forwarded header received by the downstream service.

Expected Behavior (per RFC 7239)

The gateway should append its own forwarding entry to the existing list:

Forwarded: host=upstream.example.com;proto=https, host=intermediate.example.com;proto=https

This preserves the full proxy chain so downstream services can reconstruct the original request context (e.g., for generating correct URLs).

Actual Behavior

The upstream entry host=upstream.example.com;proto=https is silently dropped. The downstream service receives only:

Forwarded: host=intermediate.example.com;proto=https

All information about the original request — including the original hostname used by the client — is lost.

Cause

In ForwardedHeadersFilter.filter(), the loop that re-emits existing Forwarded entries has the following condition:

for (Forwarded f : forwardeds) {
    // only add if "for" value matches trustedProxies
    String forValue = f.get("for");
    if (forValue != null && trustedProxies.isTrusted(forValue)) {
        updated.add(FORWARDED_HEADER, f.toHeaderValue());
    }
}

This condition requires the existing entry to have a for field and for that value to match the trusted proxies pattern. Any entry lacking a for field is unconditionally dropped.

Why this seems wrong (or at least confusing)

1. The for parameter is optional per RFC 7239

RFC 7239, Section 4, defines the ABNF as:

forwarded-element = [ forwarded-pair ] *( ";" [ forwarded-pair ] )
forwarded-pair    = token "=" value

All parameters (for, host, proto, by) are individually optional. An entry containing only host and proto is perfectly valid. The RFC says nothing that requires entries without a for field to be treated as invalid or dropped.

2. The trust check is already done on the remote address

Earlier in the same filter() method, the remote address of the incoming connection is already checked against the trusted proxies list:

if (request.getRemoteAddress() != null
        && !trustedProxies.isTrusted(request.getRemoteAddress().getHostString())) {
    return removeForwardedHeaders(input, exchange);
}

If the request comes from an untrusted remote address, all Forwarded headers are removed. If execution continues past this guard, the request is already established to be from a trusted source.

3. trusted-proxies semantics

Assuming we have one upstream proxy (upstream.example.com) before Spring Cloud Gateway (intermediate.example.com) and a client with IP 1.1.1.1 sends a request:

If the upstream proxy server adds a for field when forwarding the request to Spring Cloud Gateway, wouldn't it set it to the client IP and not its own?:

Forwarded: host=upstream.example.com;for:1.1.1.1
Host: intermediate.example.com

See https://datatracker.ietf.org/doc/html/rfc7239#section-5.2

  In a chain of proxy servers where this is fully utilized, the first
   "for" parameter will disclose the client where the request was first
   made, followed by any subsequent proxy identifiers.  The last proxy
   in the chain is not part of the list of "for" parameters.  The last
   proxy's IP address, and optionally a port number, are, however,
   readily available as the remote IP address at the transport layer.
   It can, however, be more relevant to read information about the last
   proxy from preceding "Forwarded" header field's "by" parameter, if
   present.

Which means we would need to set the trusted-proxies configuration to a .* wildcard if we want to keep Forwarded headers added by upstream.example.com for clients with any IP address. This at least is confusing when considering the name trusted-proxies.

=> It seems to me that what should actually be checked is the by field instead of the for field in the Forwarded header (if it exists).

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in org.springframework.cloud.gateway.filter.headers.ForwardedHeadersFilter, especially filter(), and compare its handling of existing entries with RFC 7239 Section 5.2. Trace the trusted-proxies decision and identify existing coverage for Forwarded headers. Done means valid upstream entries without a for field are handled consistently with the agreed trust semantics and the proxy-chain behavior is covered by regression tests.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, spring, spring-boot
Domain
api, backend, networking
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
56/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.