https://bz.apache.org/bugzilla/show_bug.cgi?id=70315
Bug ID: 70315
Summary: RemoteIpValve/RemoteIpFilter perform blocking DNS
lookups for non-IP X-Forwarded-For entries when
internalProxies/trustedProxies are CIDR lists
Product: Tomcat 11
Version: 11.0.27
Hardware: PC
OS: Linux
Status: NEW
Severity: regression
Priority: P2
Component: Catalina
Assignee: [email protected]
Reporter: [email protected]
Target Milestone: -------
Since 11.0.13, `internalProxies` and `trustedProxies` accept CIDR lists and the
default `internalProxies` is a CIDR list (regex configuration was deprecated in
the same release). Since then `RemoteIpValve` and `RemoteIpFilter` check each
`X-Forwarded-For` entry with `NetMaskSet.contains(String)`:
```
RemoteIpValve.invoke()
-> isInternalProxy(entry) / isTrustedProxy(entry)
-> checkIsCidr(netMaskSet, entry)
-> NetMaskSet.contains(String)
-> InetAddress.getByName(entry)
```
For IP literals this is cheap. Any other value, such as `unknown` (sent by some
forward proxies), an RFC 7239-style obfuscated identifier such as `_hidden`, a
hostname, or arbitrary text, is resolved through the system resolver on the
request-processing thread. The `UnknownHostException` is caught and only logged
at debug level, so the request succeeds, but only after the resolver gives up.
This is independent of the connector's `enableLookups` (default `false`), which
only controls the reverse lookup for `getRemoteHost()`.
With regex matching (the default up to 11.0.12 and in 10.1.x), such entries
simply did not match, and no lookup happened. So this is a behavioural
regression for anyone relying on the defaults.
**Impact seen in production-like environment**
We run Spring Boot 4.x (Tomcat 11.0.27), which enables RemoteIpValve
automatically on Kubernetes with a CIDR `internalProxies` default. A corporate
forward proxy put `unknown` into `X-Forwarded-For`, and the header reached the
application as `unknown, <internal proxy IPs>`. The first such request per JVM
after ~10 s (the JVM negative DNS cache) took ~5 s, which was the resolver
timeout in the pod. Nothing was logged; the time showed up as application time.
Services on Tomcat 10.1 (regex default) behind the same proxies were not
affected.
**Steps to reproduce (plain embedded Tomcat 11.0.27, no Spring)**
1. Embedded Tomcat on port 18081 with one servlet that prints
`request.getRemoteAddr()`, and a `RemoteIpValve` with default settings added to
the engine pipeline. Connector `enableLookups=false` (default).
2. To make lookups visible, a JDK 18+ `InetAddressResolverProvider` that logs
every name passed to the system resolver and then delegates to the built-in
resolver.
3. Send requests from 127.0.0.1 with various `X-Forwarded-For` values, then
repeat with `internalProxies` set to a regex covering 10/8, 192.168/16,
169.254/16, 127/8 and ::1 (a subset of the former default regex; none of the
test values falls into the omitted ranges).
```
curl -H 'X-Forwarded-For: unknown' http://127.0.0.1:18081/
```
Results (fast local resolver; in environments where the resolver times out,
each lookup takes seconds):
| X-Forwarded-For | default (CIDR) internalProxies | regex internalProxies |
|---|---|---|
| `192.0.2.1` | 4 ms, no lookup | 4 ms, no lookup |
| `unknown` (first request) | 139 ms, **DNS lookup `unknown`** on
`http-nio-…-exec-4` | 3 ms, no lookup |
| `unknown` (repeated within 10 s) | 3 ms (negative cache) | 2 ms |
| three different random tokens | 130–224 ms each, **one DNS lookup per token**
| 2–3 ms, no lookup |
| `192.0.2.1, localhost` | `localhost` resolved, treated as an internal proxy
and skipped; remoteAddr = `192.0.2.1` | remoteAddr = `localhost` |
Server-side log from the resolver hook (CIDR mode):
```
DNS-LOOKUP name=unknown took=131 ms thread=http-nio-127.0.0.1-18081-exec-4
DNS-LOOKUP name=x-440014298 took=221 ms thread=http-nio-127.0.0.1-18081-exec-9
DNS-LOOKUP name=localhost took=0 ms thread=http-nio-127.0.0.1-18081-exec-4
```
The last table row also shows a semantic difference: with CIDR lists, a
hostname in the chain is resolved and matched as if it were the proxy's
address, while with regex it is (correctly, in our view) treated as a non-proxy
entry.
**Expected behaviour**
Matching header entries against CIDR proxy lists should never trigger name
resolution. An entry that is not an IP literal cannot be inside a CIDR block.
It should be treated as "not an internal/trusted proxy", so the walk stops
there, as it did with regex matching.
**Suggested fix**
In `checkIsCidr()` (RemoteIpValve and RemoteIpFilter), or in
`NetMaskSet.contains(String)`, accept only IP literals and return `false` for
anything else without calling `InetAddress.getByName()`. Tomcat currently has
no public literal-only helper (`HttpParser.readHostIPv4/readHostIPv6` are
package-private), so this needs a small IPv4/IPv6 literal check. On Java 22+,
`InetAddress.ofLiteral(String)` does exactly this, but Tomcat 12 still supports
Java 21, so an own literal check is needed for now. A debug-level log for
skipped non-IP entries would help diagnosis.
**Workaround**
Configure `internalProxies` (and `trustedProxies`) as regular expressions. This
is not a long-term option, because regex configuration is deprecated since
11.0.13 and removed in Tomcat 12 (12.0.0-M1 changelog).
--
You are receiving this mail because:
You are the assignee for the bug.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]