1
0

Add the ssrf module

This commit is contained in:
2026-08-29 09:31:09 +05:30
parent 0fceb2cd4e
commit 4e37a54e92
28 changed files with 1391 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
[← 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<String>, 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(<the 25 CIDRs>).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(<the ranges you want to forbid>)` |
[Where the filter runs &rarr;](04-where-the-filter-runs.md)