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

2.9 KiB

← Wiring it up · Module README · Composing filters →

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:

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:

assertThat(InetAddressFilter.externalAddresses()
        .matches(InetAddress.getByName("169.254.169.254"))).isFalse();

FilterMatrix 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 is not something you would find by reading.

The diagnostic endpoint

/diag/filter?host=... reports whether a filter bean is present, whether the settings picked it up, and the verdict on every address the host resolves to:

{ "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 →