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 on ankurm.com.
One application, one deliberately vulnerable endpoint, and five filter configurations selected
by Spring profile. Every transcript under 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 |
| 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
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
- What SSRF actually costs you
- The exploit, start to finish
matches means allow — the one that matters
- Where the filter runs depends on your HTTP client
- Wiring it up, and the three ways it silently does nothing
- Operating it
- Composing filters, and the vararg that matches nothing
Captured output
The three findings worth carrying away
matches means allow. The release notes say "block"; the reference documentation says
"only allow". The second is right. Writing of(<ranges to forbid>) produces a filter that
permits exactly what you meant to stop.
internalAddresses().negate() is not externalAddresses(). They disagree on CGNAT
space, 0.0.0.0, TEST-NET-1 and multicast — the negation allows all four.
and("a", "b") matches nothing. Each address becomes a separate filter and they are
ANDed. Wrap multiple addresses in of(...) first.