ssrf-guard is four independent gates, each closing a different SSRF bypass. Read this to know what each gate guarantees, where it doesn’t, and what you still need to think about.
| Gate | When | What it checks | Failure mode |
|---|---|---|---|
SsrfGuardInterceptor |
Before DNS | URL scheme, host (whitelist match), port | SecurityException |
SafeDnsResolver (whitelist) |
At DNS resolution | Host (whitelist match again) | UnknownHostException |
SafeDnsResolver (IP filter) |
At DNS resolution | Each resolved IP is not in private/loopback/link-local/multicast/CGNAT/benchmark/IPv6-ULA | UnknownHostException if nothing left after filter |
SafeRedirectStrategy |
On every 3xx | Re-runs scheme + DNS-resolver against the redirect target | RedirectException |
Each gate runs even if the others have already passed — defense in depth. An attacker has to bypass every layer to land an outbound call.
The naïve “check the URL once, then make the request” pattern has a race condition:
Between (2) and (4) the URL string changes meaning. ssrf-guard solves this two ways:
SafeDnsResolver.resolve()), so the host the resolver sees has to be the same one the interceptor accepted.InetAddress[] is what HttpClient passes to Socket.connect() directly — there is no second DNS lookup between resolve and connect. The IP the resolver validated is the IP the socket opens to.That’s the “TOCTOU mitigation” line in the project description.
block-private-networks blocksSet to true by default. Resolves matching any of:
127.0.0.0/8, ::110.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16169.254.0.0/16 (includes AWS metadata at 169.254.169.254), fe80::/10100.64.0.0/10198.18.0.0/15224.0.0.0/4, ff00::/8fc00::/70.0.0.0, ::255.255.255.255Java’s built-in InetAddress.isSiteLocalAddress() misses CGNAT, the benchmark range, and most of the IPv6 categories — NetUtil.isPrivateOrLocal() is hand-rolled to cover all of them.
Honest list. Knowing the boundary is part of the threat model.
URI constructor, Spring’s UriComponentsBuilder, Apache HttpClient’s request line — they don’t always agree on what host a ://user:pass@a.com\@b.com/ string represents. If you accept URLs from untrusted input, normalize them before handing them to RestClient. The OWASP cheat sheet has a URL parser confusion section worth reading.RestClient HTTP clients. Code using HttpURLConnection, OkHttpClient, raw Apache HttpClient (not the wrapped one), WebClient — none of those go through the SSRF policy. The auto-configuration only customizes Spring Boot’s auto-configured RestClient.Builder.networkaddress.cache.ttl to something sane (default in modern JVMs is already 30 seconds).exact-hosts. If you whitelist 10.0.0.5, the interceptor accepts that host, and the DNS resolver short-circuits to that IP. The private-IP filter still applies (unless you set block-private-networks=false), so the request is rejected — but the layering is “interceptor accepts, resolver rejects,” not “interceptor rejects up front.”If you’re using ssrf-guard, you should also:
block-private-networks=true unless you have a specific reason to allow internal calls. The default is true precisely because turning it off is the most common way to accidentally re-enable SSRF.follow-redirects=true unless you have a specific reason to forbid redirects. Disabling redirects is sometimes a defense in depth move but it tends to break legitimate API integrations.https://api.partner.com/proxy?target= + user-supplied URL is its own SSRF (you become the attacker’s proxy). Validate the user-supplied URL before composing.SecurityException: Host not allowed in logs. Either it’s an attacker probing, or it’s a legitimate integration that needs a whitelist update.The OWASP SSRF prevention cheat sheet is worth re-reading every six months.