The StripUntrustedProxyHeadersHandler class is documented to "strip out any X-Forwarded-* headers from inbound http requests if connection is not trusted" (class Javadoc, line 37). However, the actual implementation omits several critical proxy headers from the strip set, most notably X-Forwarded-Host, which is then trusted and forwarded to origin services.
This creates a gap between the documented security posture and actual behavior when operators configure AllowWhen.NEVER (the recommended secure configuration per the Zuul wiki for deployments without a trusted ELB in front).
Affected Code
File: zuul-core/src/main/java/com/netflix/netty/common/proxyprotocol/StripUntrustedProxyHeadersHandler.java
Lines 50-55:
private static final Collection<AsciiString> HEADERS_TO_STRIP = Sets.newHashSet(
new AsciiString("x-forwarded-for"),
new AsciiString("x-forwarded-port"),
new AsciiString("x-forwarded-proto"),
new AsciiString("x-forwarded-proto-version"),
new AsciiString("x-real-ip"));
Missing from strip set:
x-forwarded-host
x-forwarded-prefix
- RFC 7239
forwarded
Impact
When StripUntrustedProxyHeadersHandler is configured with AllowWhen.NEVER (the recommended secure configuration per the Zuul wiki), operators reasonably expect that client-supplied X-Forwarded-* headers are removed. However, X-Forwarded-Host is not stripped.
Note: The framework default is AllowWhen.ALWAYS per CommonChannelConfigKeys.java, but the security-conscious configuration recommended by Netflix is NEVER for deployments without a trusted load balancer.
The method HttpRequestMessageImpl.getOriginalHost() (lines 505-515) then trusts X-Forwarded-Host with top priority over the actual Host header:
static String getOriginalHost(Headers headers, String serverName) throws URISyntaxException {
String xForwardedHost = headers.getFirst(HttpHeaderNames.X_FORWARDED_HOST);
if (xForwardedHost != null) {
return xForwardedHost; // client-controlled, returned immediately
}
// ... fallback to Host header
}
This value is then:
- Forwarded to origin services via
ProxyUtils.addXForwardedHeaders() (with overwrite=false preserving the client value)
- Used in
reconstructURI() (line 645) where it appears in access logs and response headers (for example, in the sample's X-Originating-URL)
Exploitation Scenario
Consider a deployment configured with AllowWhen.NEVER (following Netflix's security recommendation) to reject client proxy headers:
Attacker request:
GET /account/reset HTTP/1.1
Host: gateway.example.com
X-Forwarded-Host: evil.attacker.com
What happens:
- The
stripXFFHeaders() method removes X-Forwarded-For, -Port, -Proto, and -Real-IP
- But
X-Forwarded-Host: evil.attacker.com remains untouched
- The
getOriginalHost() method returns evil.attacker.com
- The origin service receives:
X-Forwarded-Host: evil.attacker.com
If the origin service builds absolute URLs from X-Forwarded-Host (password reset links, redirects) or uses it as a cache key, this enables:
- Password reset link poisoning
- Web cache poisoning (if cache is keyed on host)
- Open redirect (if origin trusts the header for redirects)
Scope Note
This finding is about defense-in-depth and hardening. The actual impact depends on downstream origin behavior:
- Zuul itself is not directly vulnerable (routing is VIP-based, not header-based)
- Origin services that trust
X-Forwarded-Host for URL construction or caching are at risk
- Operators deploying Zuul with the
NEVER policy reasonably expect full X-Forwarded-* protection based on the Javadoc
Recommended Fix
Option 1 (minimal change): Add the missing headers to HEADERS_TO_STRIP:
private static final Collection<AsciiString> HEADERS_TO_STRIP = Sets.newHashSet(
new AsciiString("x-forwarded-for"),
new AsciiString("x-forwarded-host"), // ADD THIS
new AsciiString("x-forwarded-prefix"), // ADD THIS
new AsciiString("x-forwarded-port"),
new AsciiString("x-forwarded-proto"),
new AsciiString("x-forwarded-proto-version"),
new AsciiString("x-real-ip"),
new AsciiString("forwarded")); // ADD THIS (RFC 7239)
Option 2 (defense-in-depth): Make getOriginalHost() prefer the validated Host header when AllowWhen.NEVER is active, rather than trusting X-Forwarded-Host unconditionally.
Option 3 (documentation): If the current behavior is intentional, update the class Javadoc to clarify that X-Forwarded-Host and Forwarded are not stripped even in NEVER mode.
Verified Safe Areas (for context)
I reviewed the following areas and confirmed they are not vulnerable:
- HTTP/1.1 request smuggling protections (
Http1FramingEnforcingHandler) are solid
- HTTP/2 per-stream content-length enforcement is working correctly
- Multiple
Host headers are properly rejected
- Routing is VIP-based (no SSRF via
Host or X-Forwarded-Host injection)
Steps to Reproduce
- Deploy Zuul with
StripUntrustedProxyHeadersHandler(AllowWhen.NEVER)
- Send the following request:
GET / HTTP/1.1
Host: gateway.example.com
X-Forwarded-Host: attacker.com
- Observe in origin logs or Zuul's
X-Originating-URL response header that it shows http://attacker.com/ (the attacker-controlled host)
References
- RFC 7230 § 5.4: Host header handling
- RFC 7239: Forwarded HTTP Extension (standardized proxy headers)
- OWASP: Web Cache Poisoning via Host Header Injection
Additional Context
This gap was discovered during a security audit of Zuul's proxy header handling as part of research into API gateway hardening patterns. The audit also confirmed that Zuul's HTTP/1.1 and HTTP/2 request smuggling protections are working correctly (see "Verified Safe Areas" above).
Disclosure
- Discovered: 2026-07-03
- Researcher: refleeexzz
- Coordination: Reporting publicly via GitHub as this is a defense-in-depth gap with no immediate production impact to Netflix deployments
I'm happy to provide a PR implementing any of the recommended fixes if the maintainers would like. Thank you for maintaining Zuul and for considering this report!
The
StripUntrustedProxyHeadersHandlerclass is documented to "strip out any X-Forwarded-* headers from inbound http requests if connection is not trusted" (class Javadoc, line 37). However, the actual implementation omits several critical proxy headers from the strip set, most notablyX-Forwarded-Host, which is then trusted and forwarded to origin services.This creates a gap between the documented security posture and actual behavior when operators configure
AllowWhen.NEVER(the recommended secure configuration per the Zuul wiki for deployments without a trusted ELB in front).Affected Code
File:
zuul-core/src/main/java/com/netflix/netty/common/proxyprotocol/StripUntrustedProxyHeadersHandler.javaLines 50-55:
Missing from strip set:
x-forwarded-hostx-forwarded-prefixforwardedImpact
When
StripUntrustedProxyHeadersHandleris configured withAllowWhen.NEVER(the recommended secure configuration per the Zuul wiki), operators reasonably expect that client-suppliedX-Forwarded-*headers are removed. However,X-Forwarded-Hostis not stripped.Note: The framework default is
AllowWhen.ALWAYSperCommonChannelConfigKeys.java, but the security-conscious configuration recommended by Netflix isNEVERfor deployments without a trusted load balancer.The method
HttpRequestMessageImpl.getOriginalHost()(lines 505-515) then trustsX-Forwarded-Hostwith top priority over the actualHostheader:This value is then:
ProxyUtils.addXForwardedHeaders()(withoverwrite=falsepreserving the client value)reconstructURI()(line 645) where it appears in access logs and response headers (for example, in the sample'sX-Originating-URL)Exploitation Scenario
Consider a deployment configured with
AllowWhen.NEVER(following Netflix's security recommendation) to reject client proxy headers:Attacker request:
What happens:
stripXFFHeaders()method removesX-Forwarded-For,-Port,-Proto, and-Real-IPX-Forwarded-Host: evil.attacker.comremains untouchedgetOriginalHost()method returnsevil.attacker.comX-Forwarded-Host: evil.attacker.comIf the origin service builds absolute URLs from
X-Forwarded-Host(password reset links, redirects) or uses it as a cache key, this enables:Scope Note
This finding is about defense-in-depth and hardening. The actual impact depends on downstream origin behavior:
X-Forwarded-Hostfor URL construction or caching are at riskNEVERpolicy reasonably expect fullX-Forwarded-*protection based on the JavadocRecommended Fix
Option 1 (minimal change): Add the missing headers to
HEADERS_TO_STRIP:Option 2 (defense-in-depth): Make
getOriginalHost()prefer the validatedHostheader whenAllowWhen.NEVERis active, rather than trustingX-Forwarded-Hostunconditionally.Option 3 (documentation): If the current behavior is intentional, update the class Javadoc to clarify that
X-Forwarded-HostandForwardedare not stripped even inNEVERmode.Verified Safe Areas (for context)
I reviewed the following areas and confirmed they are not vulnerable:
Http1FramingEnforcingHandler) are solidHostheaders are properly rejectedHostorX-Forwarded-Hostinjection)Steps to Reproduce
StripUntrustedProxyHeadersHandler(AllowWhen.NEVER)X-Originating-URLresponse header that it showshttp://attacker.com/(the attacker-controlled host)References
Additional Context
This gap was discovered during a security audit of Zuul's proxy header handling as part of research into API gateway hardening patterns. The audit also confirmed that Zuul's HTTP/1.1 and HTTP/2 request smuggling protections are working correctly (see "Verified Safe Areas" above).
Disclosure
I'm happy to provide a PR implementing any of the recommended fixes if the maintainers would like. Thank you for maintaining Zuul and for considering this report!