[← The exploit](02-the-exploit.md) · [Module README](../README.md) · [Where the filter runs →](04-where-the-filter-runs.md) # 3. `matches` means allow This is the chapter that matters. Get this backwards and you ship a service that still leaks and also cannot make its own outbound calls. ## What the sources say The Spring Boot 4.1 release notes: > Both reactive and blocking HTTP clients can now be configured with an `InetAddressFilter` > which can **block** outgoing requests to specific addresses. The reference documentation, one click further in: > To limit the address that a client can call, you can use an `InetAddressFilter` which will > **only allow** outgoing calls to addresses that match the filter. Those describe opposite configurations, and the release-notes sentence is the one that got copied into the write-ups. The reference documentation is the correct one, and the bytecode agrees with it. `FilteredAddresses.of(stream, predicate)` filters the resolved addresses *through* the predicate and keeps what matches; `Filtered.orElseThrow` raises `FilteredHostException` when nothing is left: ``` T orElseThrow(Supplier, InetAddressFilter): if (result == null || check.test(result)) throw new FilteredHostException(...) return result ``` So: **the filter is an allow-list. An address that matches is permitted. An address that does not match is dropped, and if every address is dropped the call fails.** ## The inversion, run The `blocklist` profile is what you write if you act on the word "block" — name the private ranges you want forbidden: ```java InetAddressFilter.of("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"); ``` From [`docs/output/exploit-by-profile.txt`](output/exploit-by-profile.txt): ``` http://127.0.0.1:8080/internal/credentials BLOCKED_BY_FILTER | Filtered host '127.0.0.1' http://172.16.10.3:8080/internal/credentials FETCHED | {"Expiration":"2026-08-... http://example.com/ BLOCKED_BY_FILTER | Filtered host 'example.com' ``` Read those three lines together. The RFC 1918 target — the one the configuration was written to forbid — **succeeds**. The legitimate call to `example.com` **fails**. The configuration achieved precisely the opposite of its intent in both directions. The loopback row still blocks, which is the cruel part: the naive exploit everyone tests with stops working, so the change looks like it worked. ## What the factory methods actually contain `specialPurpose()` is documented as "special purpose IP addresses as defined by RFC 6890". Its constant pool holds 25 CIDR strings, and **none of them is an RFC 1918 range**: ``` 0.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16 192.0.0.0/24 192.0.0.0/29 192.0.2.0/24 192.88.99.0/24 198.18.0.0/15 198.51.100.0/24 203.0.113.0/24 240.0.0.0/4 255.255.255.255/32 ::/128 ::1/128 64:ff9b::/96 100::/64 2001::/23 2001::/32 2001:2::/48 2001:db8::/32 2001:10::/28 2002::/16 fc00::/7 fe80::/10 ``` It still matches `10.0.0.1`, because the method is not just that list: ```java specialPurpose() = of().or(InternalInetAddressFilter.instance) ``` and `InternalInetAddressFilter` is `isLoopbackAddress() || isLinkLocalAddress() || isSiteLocalAddress()`, plus an IPv6 arm that also decodes **NAT64-embedded** addresses (`64:ff9b::a00:1` is `10.0.0.1` wearing a hat) and re-tests the embedded IPv4. The RFC 1918 coverage comes from the JDK's own predicates, not from the registry list. That is worth knowing before you build anything on top of `specialPurpose()`, because its name and its javadoc both suggest it is the RFC 6890 registry and only the registry. ## `internalAddresses().negate()` is not `externalAddresses()` They look interchangeable. They are not, and [`docs/output/filter-matrix.txt`](output/filter-matrix.txt) has the rows: | Address | `externalAddresses()` | `internalAddresses().negate()` | |---|---|---| | `100.64.0.1` (CGNAT) | `false` | **`true`** | | `0.0.0.0` | `false` | **`true`** | | `192.0.2.1` (TEST-NET-1) | `false` | **`true`** | | `224.0.0.1` (multicast) | `false` | **`true`** | `internalAddresses()` is `routable().and(InternalInetAddressFilter.instance)` — loopback, link-local and site-local, nothing else. Negating it allows everything that is none of those, and "none of those" includes carrier-grade NAT space, which on a mobile or ISP-adjacent network is emphatically not the public internet. `externalAddresses()` is `routable().andNot(multicast(), specialPurpose())`, which is a different and stricter statement. Prefer it. ## The short version | Intent | Write | |---|---| | only call the public internet | `InetAddressFilter.externalAddresses()` | | only call these destinations | `InetAddressFilter.of("203.0.113.0/24", "198.51.100.7")` | | public internet minus a range | `externalAddresses().andNot("203.0.113.0/24")` | | **never** | `InetAddressFilter.of()` | [Where the filter runs →](04-where-the-filter-runs.md)