flawopen.com/ssrf/Java
Server-Side Request Forgery (SSRF) in Java (OkHttp, Spring)
new URL(userUrl).openStream() lets a Spring service read file:, jar: and internal http: URLs, while isSiteLocalAddress() misses loopback and link-local addresses. DNS rebinding and redirects can bypass an earlier check. See how OkHttp Dns filters listed address classes, why IP literals need checking, and why network egress policy remains necessary.
Imagine giving a visitor a universal building pass that opens the web lobby, the records room, and the loading dock. java.net.URL is similarly broad: Java 21 guarantees handlers for file:, jar:, http:, and https:, while applications may provide additional protocols. A user-controlled value can select both a destination and a protocol. Calling openStream() uses the selected protocol handler. The safer Java example switches to OkHttp HttpUrl for HTTP(S), filters resolved hosts through OkHttp Dns, checks IP literals too, and disables redirects and proxies. Think of Dns as the guard checking each address the courier will actually use, while followRedirects(false) prevents a public desk from handing over a new route into a restricted room.
Core Concepts & Subsystem Terms
URL.openStream() and non-HTTP schemesjava.net.URLuses protocol handlers. Java 21 guarantees handlers forfile:,jar:,http:, andhttps:; additional protocols may be available through providers. ThusopenStream()on afile:URL can read a local file. For HTTP, it also follows redirects by default, so a public URL can bounce a request to an internal one.InetAddress.isSiteLocalAddress() is not enough- It only covers
10/8,172.16/12,192.168/16and the deprecatedfec0::/10.127.0.0.1,169.254.169.254,0.0.0.0,100.64.0.0/10and IPv6 unique-localfc00::/7all pass, andgetByNamereturns just one of the host's addresses. Redirects and check-then-fetch- OkHttp follows redirects by default, and it resolves the hostname again when it connects. A DNS server with a short TTL can return a public address to the check and
127.0.0.1to OkHttp, and a302tohttp://169.254.169.254/works even when the original URL was public. OkHttp Dns- OkHttp connects only to the addresses returned by the client's
Dns.lookup, for the first request and for every redirect or retry, so filtering there checks the addresses actually used. OkHttp does not callDnsfor IP-literal hosts such ashttp://127.0.0.1/, so check those before the call, and setProxy.NO_PROXYso a proxy does not bypass the filter. This sample blocks only the classes shown; it is not a complete IANA global-reachability test. Keep the range policy current and enforce egress independently. IPv4-mapped addresses in InetAddressInetAddress.getByName("::ffff:127.0.0.1")returns anInet4Address, so IPv4-mapped IPv6 is classified as IPv4. TheisLoopbackAddress,isLinkLocalAddress,isSiteLocalAddressandisAnyLocalAddressmethods cover most ranges; CGNAT andfc00::/7need explicit byte checks.
Framework mappings
- CWE reference for the weakness described in this guide: CWE-918
- OWASP Top 10:2025 — A01:2025 Broken Access Control
- OWASP API Security Top 10 — API7:2023 Server Side Request Forgery
- D3-OTF Outbound Traffic Filtering — This guide's outbound network restriction limits which destinations the service or host can reach.
Step-by-Step Attack Flow
A user supplies a URL
A Spring controller receives url in a JSON body and passes it to URL.openStream, RestTemplate, WebClient or OkHttp.
The service connects to an internal address
The URL names 127.0.0.1, a private host, 169.254.169.254, a file: path, a hostname that rebinds, or a public page that redirects inward. The benign test value http://127.0.0.1:8080/actuator/health illustrates the case.
The internal service answers the trusted caller
Spring Boot Actuator, admin consoles and the metadata service accept the request because it comes from inside the network.
The attacker reads secrets or pivots
The response or its status leaks credentials, environment values or open ports, and cloud keys can be reused from outside.
Source Code: Flaw vs. Secure Implementation
// FetchController.java: both endpoints let the caller choose the destination
package com.example.fetch;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
@RestController
public class FetchController {
record FetchRequest(String url) {}
static final OkHttpClient CLIENT = new OkHttpClient();
// java.net.URL also opens file: and jar: URLs and follows redirects
@PostMapping("/preview")
public String preview(@RequestBody FetchRequest in) throws IOException {
try (InputStream stream = new URL(in.url()).openStream()) {
return new String(stream.readNBytes(5000), StandardCharsets.UTF_8);
}
}
// isSiteLocalAddress misses 127.0.0.1 and 169.254.169.254; OkHttp resolves again
@PostMapping("/webhooks/test")
public Map<String, Integer> testWebhook(@RequestBody FetchRequest in) throws IOException {
InetAddress address = InetAddress.getByName(new URL(in.url()).getHost());
if (address.isSiteLocalAddress()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
RequestBody body = RequestBody.create("{\"ping\":true}", MediaType.get("application/json"));
Request request = new Request.Builder().url(in.url()).post(body).build();
try (Response response = CLIENT.newCall(request).execute()) {
return Map.of("status", response.code());
}
}
}
// OkHttp applies a baseline address filter; independently enforce current network egress policy
package com.example.fetch;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Proxy;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import okhttp3.Dns;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
@RestController
public class FetchController {
record FetchRequest(String url) {}
// baseline filter only; this is not a complete IANA global-reachability classifier
static boolean passesBaselineFilter(InetAddress a) {
byte[] b = a.getAddress();
boolean cgnat = b.length == 4 && (b[0] & 0xff) == 100 && (b[1] & 0xc0) == 64;
boolean uniqueLocal = b.length == 16 && (b[0] & 0xfe) == 0xfc;
return !(a.isAnyLocalAddress() || a.isLoopbackAddress() || a.isLinkLocalAddress()
|| a.isSiteLocalAddress() || a.isMulticastAddress() || cgnat || uniqueLocal);
}
// OkHttp connects only to what this returns, for every request and redirect
static final Dns FILTERED_DNS = hostname -> {
List<InetAddress> addresses = Dns.SYSTEM.lookup(hostname);
if (addresses.isEmpty() || !addresses.stream().allMatch(FetchController::passesBaselineFilter)) {
throw new UnknownHostException("blocked destination " + hostname);
}
return addresses;
};
// no proxy, no redirects, timeouts; IP literals skip Dns, so check them first
static final OkHttpClient CLIENT = new OkHttpClient.Builder()
.dns(FILTERED_DNS)
.proxy(Proxy.NO_PROXY)
.followRedirects(false)
.followSslRedirects(false)
.connectTimeout(3, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.callTimeout(10, TimeUnit.SECONDS)
.build();
static HttpUrl checkedUrl(String raw) throws UnknownHostException {
HttpUrl url = HttpUrl.parse(raw);
if (url == null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "only http and https URLs are allowed");
}
FILTERED_DNS.lookup(url.host());
return url;
}
// at most 5000 bytes are read from any response
@PostMapping("/preview")
public String preview(@RequestBody FetchRequest in) throws IOException {
Request request = new Request.Builder().url(checkedUrl(in.url())).build();
try (Response response = CLIENT.newCall(request).execute()) {
return response.peekBody(5000).string();
}
}
@PostMapping("/webhooks/test")
public Map<String, Integer> testWebhook(@RequestBody FetchRequest in) throws IOException {
RequestBody body = RequestBody.create("{\"ping\":true}", MediaType.get("application/json"));
Request request = new Request.Builder().url(checkedUrl(in.url())).post(body).build();
try (Response response = CLIENT.newCall(request).execute()) {
return Map.of("status", response.code());
}
}
@ExceptionHandler(UnknownHostException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
void blocked() {
}
}
Engineering & System Hardening Checklist
- ✓Enable Find Security Bugs
URLCONNECTION_SSRF_FDin SpotBugs and review every outbound call that takes a user-supplied URL. - ✓Never call
URL.openStreamoropenConnectionon user input; parse withHttpUrl.parse, which accepts onlyhttpandhttps. - ✓Give OkHttp a
Dnsthat rejects loopback, link-local, site-local, any-local, CGNAT andfc00::/7, check IP-literal hosts with the same function, and setProxy.NO_PROXY. - ✓Disable redirects (
followRedirects(false),followSslRedirects(false)), set connect, read and call timeouts, and read at most a fixed number of bytes. - ✓Enforce IMDSv2 with a hop limit of 1, keep Actuator endpoints off the public interface, and restrict egress with a firewall as a second layer.
- ✓The address checks above are a baseline, not proof that every accepted destination is globally reachable. Maintain an IANA-informed special-purpose range policy, prefer an explicit host allowlist where feasible, and enforce egress restrictions independently.
Sources
- CWE-918: Server-Side Request Forgery (SSRF)
- OkHttp — Dns interface
- OkHttp — OkHttpClient.Builder (followRedirects, proxy)
- Java SE 21 API — InetAddress
- Find Security Bugs — URLCONNECTION_SSRF_FD
- OWASP Server-Side Request Forgery Prevention Cheat Sheet
- IANA IPv4 Special-Purpose Address Registry
- IANA IPv6 Special-Purpose Address Registry
- Java SE 21 API — URL protocol handlers and openStream()