1
0
Files
spring-security-demo/ssrf/README.md
2026-08-29 09:31:09 +05:30

88 lines
4.1 KiB
Markdown

# `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(<ranges to forbid>)` 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.