[BUG] ForwardedRemoteAddressResolver inverted guard discards client IP + blocking DNS on unvalidated X-Forwarded-For
- Dominant language
- Java
- Stars
- 8.8k
- Forks
- 3.1k
- Avg merge
- 7d 1h
- Merged PRs (30d)
- 85
Description
## Description
`ForwardedRemoteAddressResolver` (the production default, wired as `new ForwardedRemoteAddressResolver(1)` in `ShenyuConfiguration`) has an inverted guard in `extractXForwardedValues`: after splitting `X-Forwarded-For` by `", "`, it does
```java
if (values.size() == 1 && StringUtils.isNotEmpty(values.get(0))) {
return Collections.emptyList();
}
```
`isNotEmpty` is the opposite of the intended check. The intent is to discard a single *empty* value (blank header), but the code discards a single *non-empty* value — i.e. a valid single client IP. `resolve()` then falls through to the TCP peer address (the proxy's IP), discarding the real client IP. Conversely, an empty-value header is *not* discarded and produces `new InetSocketAddress("", 0)` → wildcard `0.0.0.0`.
Additionally `resolve()` constructs `new InetSocketAddress(xForwardedValues.get(index), 0)`; the `InetSocketAddress(String,int)` ctor performs a **blocking DNS lookup** when the value is not a literal IP. `X-Forwarded-For` is unvalidated client input; with the default `maxTrustedIndex=1` the leftmost (most spoofable) value is selected. When `shenyu.scheduler.enabled=false` (default), `HostAddressUtils.acquireIp` runs on the Netty event loop → blocking DNS stalls all connections on that thread (DoS) and enables DNS exfiltration/reconnaissance.
## Location
- `shenyu-web/src/main/java/org/apache/shenyu/web/forward/ForwardedRemoteAddressResolver.java:89-108` (inverted guard :106-108; blocking DNS :89-90)
- `shenyu-spring-boot-starter/shenyu-spring-boot-starter-gateway/src/main/java/org/apache/shenyu/springboot/starter/gateway/ShenyuConfiguration.java:158-160` (production default)
## Impact
- Default deployment (single-hop trusted proxy appending one client IP — the most common case) silently ignores the client IP. `HostAddressUtils.acquireIp(exchange)` (consumed by `AbstractLoggingPlugin` for `clientIp`, and by any IP-based allow/deny/rate-limit feature) returns the proxy's IP. Access logs, rate limiting, and IP-based security all see the wrong IP.
- DoS: crafted `X-Forwarded-For` hostnames trigger blocking DNS (up to system DNS timeout, 5–15s) on the event loop.
- DNS exfiltration: gateway resolves attacker-controlled hostnames, leaking data to attacker's DNS server.
## Suggested fix
- Change `StringUtils.isNotEmpty(values.get(0))` → `StringUtils.isEmpty(values.get(0))` so only empty single values are discarded.
- Validate the selected value is a literal IP (`InetAddressUtils.isIPv4/IPv6` or try `InetAddress.getByAddress`) before constructing `InetSocketAddress`; otherwise fall back to the TCP remote address. Never call `new InetSocketAddress(String,int)` with unvalidated input on a reactive thread.
## Related existing
None. Distinct from #6556 (WebSocket Upgrade header case-sensitivity in `DefaultShenyuContextBuilder`).
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with shenyu-web/src/main/java/org/apache/shenyu/web/forward/ForwardedRemoteAddressResolver.java, tracing extractXForwardedValues and resolve(), then check the default wiring in ShenyuConfiguration.java. Done means a single valid client IP is retained, blank or non-literal values fall back safely, and unvalidated X-Forwarded-For input cannot trigger blocking DNS on the reactive thread.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- backend-api-design, networking, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100