# `ssrf` — SSRF mitigation with `InetAddressFilter`, vulnerable endpoint included Companion project for [**HTTP Client SSRF Mitigation in Spring Boot 4.1: The `InetAddressFilter` Everyone Will Configure Backwards**](https://ankurm.com/spring-boot-4-1-ssrf-inetaddressfilter/) on ankurm.com. One application, one deliberately vulnerable endpoint, and five filter configurations selected by Spring profile. Every transcript under [`docs/output/`](docs/output/) came from running it; `./scripts/run-all.sh` regenerates all of them. ## Versions | | Version | Notes | |---|---|---| | JDK | 25 (Temurin 25.0.4.1+1) | current LTS | | Spring Boot | **4.1.1** | 4.1.0 GA was 10 June 2026; `InetAddressFilter` is `@since 4.1.0` | | Spring Framework | 7.0.9 | Boot-managed | | Apache HttpComponents | 5.6.4 | on the classpath deliberately — see [chapter 4](docs/04-where-the-filter-runs.md) | | Tomcat | 11.0.24 | | | JUnit Jupiter / AssertJ | Boot-managed | 4 assertions | Versions were read from `repo1.maven.org/.../maven-metadata.xml`, not from release announcements. ## Quickstart ```bash ./scripts/run.sh # no filter bean at all ./scripts/exploit.sh # four targets, four sets of credentials ./scripts/run.sh docsfilter # InetAddressFilter.externalAddresses() ./scripts/exploit.sh # internal targets blocked, example.com still works ./scripts/run.sh blocklist # the inversion ./scripts/exploit.sh # RFC 1918 target succeeds, example.com fails ./scripts/run-all.sh # regenerate everything under docs/output/ ``` ## Profiles | Profile | Filter bean | What it shows | |---|---|---| | *(none)* | — | the exploit, working | | `docsfilter` | `externalAddresses()` | the reference documentation's recommendation, and it is correct | | `blocklist` | `of(RFC1918)` | the release notes' word "block", acted on: attack succeeds, legitimate call fails | | `negated` | `internalAddresses().negate()` | looks equivalent to `externalAddresses()`, differs on four rows | | `allowlist` | `externalAddresses().and(of(...))` | naming your destinations, and what that costs when their DNS changes | | `twofilters` | two beans | the context does not start, and the diagnostic blames `RestClient` | ## Endpoints | Endpoint | Purpose | |---|---| | `GET /preview?url=` | the vulnerable fetcher | | `GET /internal/credentials` | the thing that must not be reachable | | `GET /diag/filter?host=` | what the running context decided. Delete before shipping | ## Documentation 1. [What SSRF actually costs you](docs/01-what-ssrf-costs-you.md) 2. [The exploit, start to finish](docs/02-the-exploit.md) 3. [`matches` means allow](docs/03-allow-not-block.md) — the one that matters 4. [Where the filter runs depends on your HTTP client](docs/04-where-the-filter-runs.md) 5. [Wiring it up, and the three ways it silently does nothing](docs/05-wiring-it-up.md) 6. [Operating it](docs/06-operating-it.md) 7. [Composing filters, and the vararg that matches nothing](docs/07-composing-filters.md) ## Captured output | File | Produced by | |---|---| | [`filter-matrix.txt`](docs/output/filter-matrix.txt) | `FilterMatrix` — 15 addresses × 9 filters | | [`exploit-by-profile.txt`](docs/output/exploit-by-profile.txt) | `run-all.sh` — five profiles, five targets each | | [`and-varargs-trap.txt`](docs/output/and-varargs-trap.txt) | `AndVarargsTrap` | | [`two-filter-beans.txt`](docs/output/two-filter-beans.txt) | the `twofilters` startup failure | | [`tests.txt`](docs/output/tests.txt) | `WhereTheFilterRunsTests` | ## The three findings worth carrying away 1. **`matches` means allow.** The release notes say "block"; the reference documentation says "only allow". The second is right. Writing `of()` produces a filter that permits exactly what you meant to stop. 2. **`internalAddresses().negate()` is not `externalAddresses()`.** They disagree on CGNAT space, `0.0.0.0`, TEST-NET-1 and multicast — the negation allows all four. 3. **`and("a", "b")` matches nothing.** Each address becomes a separate filter and they are ANDed. Wrap multiple addresses in `of(...)` first.