Add the ssrf module
This commit is contained in:
67
ssrf/docs/06-operating-it.md
Normal file
67
ssrf/docs/06-operating-it.md
Normal file
@@ -0,0 +1,67 @@
|
||||
[← Wiring it up](05-wiring-it-up.md) · [Module README](../README.md) · [Composing filters →](07-composing-filters.md)
|
||||
|
||||
# 6. Operating it
|
||||
|
||||
## What the caller sees
|
||||
|
||||
`FilteredHostException` is a plain `RuntimeException`. Uncaught in a controller it is a bare
|
||||
**HTTP 500**, and Boot's default error body does not name the host — so the first symptom in
|
||||
production is a 500 with nothing useful in the response and a stack trace in the log.
|
||||
|
||||
Catch it. It carries the two things you want:
|
||||
|
||||
```java
|
||||
catch (FilteredHostException ex) {
|
||||
log.warn("outbound call to {} blocked by {}", ex.getHost(), ex.getFilter());
|
||||
return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(...);
|
||||
}
|
||||
```
|
||||
|
||||
`getHost()` is the host **string from the URI**, not the resolved address — `Filtered host
|
||||
'localhost'`, not `Filtered host '127.0.0.1'`. On the Apache path the resolved addresses were
|
||||
available and were not kept, so if you want to log which address triggered it you have to
|
||||
resolve again yourself.
|
||||
|
||||
A 4xx is arguably the better answer when the URL came from a user, since the request is what is
|
||||
wrong. Do not echo `ex.getMessage()` back to them: it confirms which hosts are unreachable,
|
||||
which turns your error response into a network scanner. Log it, return something bland.
|
||||
|
||||
## Alert on it
|
||||
|
||||
A `FilteredHostException` is either an attack or an outage, and you want to know which. Both are
|
||||
worth paging on eventually, but the second is the one that will bite you first: an allow-list
|
||||
pinned to IP ranges fails the day the destination changes its DNS. The `allowlist` profile in
|
||||
this module was written against `93.184.216.34`, which was example.com's address for a decade
|
||||
and is not any more.
|
||||
|
||||
## Testing it
|
||||
|
||||
The filter is a `@FunctionalInterface` with no Spring dependencies, so the allow/deny decision
|
||||
is a unit test — no context, no network:
|
||||
|
||||
```java
|
||||
assertThat(InetAddressFilter.externalAddresses()
|
||||
.matches(InetAddress.getByName("169.254.169.254"))).isFalse();
|
||||
```
|
||||
|
||||
[`FilterMatrix`](../src/main/java/com/ankurm/ssrf/FilterMatrix.java) is that idea with a
|
||||
table around it. Run it against your own filter before you deploy it; the four-row disagreement
|
||||
in [chapter 3](03-allow-not-block.md) is not something you would find by reading.
|
||||
|
||||
## The diagnostic endpoint
|
||||
|
||||
[`/diag/filter?host=...`](../src/main/java/com/ankurm/ssrf/DiagnosticsController.java) reports
|
||||
whether a filter bean is present, whether the settings picked it up, and the verdict on every
|
||||
address the host resolves to:
|
||||
|
||||
```json
|
||||
{ "filterBeanPresent": true, "settingsCarryFilter": true, "host": "example.com",
|
||||
"resolvesTo": { "172.66.147.243": true, "104.20.23.154": true } }
|
||||
```
|
||||
|
||||
That answers the question you actually have when an outbound call fails, which is not "what does
|
||||
my configuration say" but "what does the running context think". **Delete it before shipping**:
|
||||
it is an oracle for your outbound allow-list and a host-resolution service for anyone who finds
|
||||
it.
|
||||
|
||||
[Composing filters →](07-composing-filters.md)
|
||||
Reference in New Issue
Block a user