Spring Boot 4.1 InetAddressFilter: Built-in SSRF Protection for HTTP Clients
Spring Boot 4.1 introduces InetAddressFilter to block internal network access at the HTTP client level, replacing scattered string checks with a centralized allowlist approach. The article demonstrates configuring RestClient.Builder for external-only access, adding URI validation, disabling auto-redirects, setting timeouts, limiting response size, and enforcing secure patterns via tests and code reviews.
SSRF Risk in a Simple Link Preview
A link preview feature fetches a user-supplied URL, extracts title, description, and image, and renders a share card. The naive implementation uses Spring's RestClient to GET the URL directly:
@RestController
@RequestMapping("/api/link")
public class LinkPreviewController {
private final RestClient restClient;
public LinkPreviewController(RestClient.Builder builder) {
this.restClient = builder.build();
}
@GetMapping("/preview")
public String preview(@RequestParam String url) {
return restClient.get()
.uri(url)
.retrieve()
.body(String.class);
}
}Because the server controls the outbound request, an attacker can supply http://127.0.0.1:8080/xxx or http://10.0.0.10/xxx to reach internal services, databases, Redis, or admin interfaces — a classic Server-Side Request Forgery (SSRF).
Why Ad-hoc String Checks Fail
Early fixes block localhost or 127.0.0.1 via string matching, but attackers bypass with 127.0.0.1, IPv6 ::1, private CIDR blocks ( 10.x.x.x, 172.16-31.x.x, 192.168.x.x), or domain names that resolve to internal IPs (e.g., internal.example.test → 192.168.1.20). A stronger approach resolves the host and checks InetAddress.isLoopbackAddress() or isSiteLocalAddress(), yet this logic ends up duplicated across every feature that calls external URLs (link preview, image download, webhook test, file sync, remote markdown import, third-party API, avatar fetch, callback verification).
Spring Boot 4.1: Centralized InetAddressFilter
Spring Boot 4.1 adds InetAddressFilter to the auto-configured HTTP client stack. It defines allowed addresses, not blocked ones. The simplest secure configuration for a public-only client:
@Configuration(proxyBeanMethods = false)
public class HttpClientSecurityConfig {
@Bean
public InetAddressFilter httpClientInetAddressFilter() {
return InetAddressFilter.externalAddresses();
}
} externalAddresses()permits only external (non-loopback, non-private) destinations. The existing RestClient code remains unchanged provided it uses the Spring-boot-managed RestClient.Builder:
@Service
public class RemotePageService {
private final RestClient restClient;
public RemotePageService(RestClient.Builder builder) {
this.restClient = builder.build();
}
public String download(String url) {
return restClient.get().uri(url).retrieve().body(String.class);
}
}If developers instantiate clients via RestClient.create() or RestClient.builder().build(), the filter is bypassed. The author replaces all such occurrences with injected RestClient.Builder and adds a code-review rule: "Business code must not create HTTP clients directly; obtain the Builder from the Spring container."
Two-Layer Defense: URI Validation + Network Filter
Layer 1 (business): validate URI syntax and scheme — HTTP/HTTPS only, host present, no user-info.
@Component
public class ExternalUrlValidator {
public URI validate(String value) {
URI uri;
try { uri = URI.create(value); }
catch (IllegalArgumentException e) { throw new InvalidUrlException("URL 格式不正确"); }
String scheme = uri.getScheme();
if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme))
throw new InvalidUrlException("只允许 HTTP 和 HTTPS");
if (uri.getHost() == null || uri.getHost().isBlank())
throw new InvalidUrlException("URL 缺少有效域名");
if (uri.getUserInfo() != null)
throw new InvalidUrlException("URL 不允许包含用户信息");
return uri;
}
}Layer 2 (network): InetAddressFilter checks the resolved IP at connection time. This separation keeps business logic clean and ensures the filter cannot be circumvented by DNS tricks.
Redirect Handling
Auto-following redirects (301/302/307/308) can redirect a safe public URL to an internal one. The author disables automatic redirects via configuration:
spring:
http:
clients:
connect-timeout: 2s
read-timeout: 3s
redirects: dont-followIf redirects are required, implement a custom follower that re-validates each Location header against the same URI and network policies, limiting hops (e.g., max 3) and allowing only http→http, http→https, https→https.
Timeouts and Response Size Limits
Unbounded reads let an attacker stall threads or exhaust memory. The final downloader uses exchange() to enforce:
Connect timeout 2s, read timeout 3s (via config)
Max body 512 KB ( input.readNBytes(MAX_BODY_SIZE + 1))
Content-Type must be HTML
Non-2xx status throws exception
public String download(String url) {
URI uri = validator.validate(url);
return restClient.get()
.uri(uri)
.exchange((request, response) -> {
if (!response.getStatusCode().is2xxSuccessful())
throw new RemotePageException("远程页面返回:" + response.getStatusCode());
MediaType contentType = response.getHeaders().getContentType();
if (contentType != null && !MediaType.TEXT_HTML.isCompatibleWith(contentType))
throw new RemotePageException("远程内容不是 HTML");
try (InputStream input = response.getBody()) {
byte[] bytes = input.readNBytes(MAX_BODY_SIZE + 1);
if (bytes.length > MAX_BODY_SIZE)
throw new RemotePageException("远程页面过大");
return new String(bytes, StandardCharsets.UTF_8);
}
});
}Error Handling Without Leaking Internals
A global exception handler catches FilteredHostException (thrown by the filter) and returns a generic 403 with title "目标地址不可访问" and detail "该 URL 指向禁止访问的网络地址", avoiding disclosure of the resolved internal IP.
Testing and Governance
Unit tests verify the filter blocks loopback and private addresses:
class InetAddressFilterTest {
private final InetAddressFilter filter = InetAddressFilter.externalAddresses();
@Test void shouldBlockLoopback() throws Exception {
InetAddress address = InetAddress.getByName("127.0.0.1");
assertThat(filter.matches(address)).isFalse();
}
@Test void shouldBlockPrivateAddress() throws Exception {
InetAddress address = InetAddress.getByName("192.168.1.10");
assertThat(filter.matches(address)).isFalse();
}
}More importantly, automated grep checks prevent regression:
grep -R "RestClient.create" src/main/java
grep -R "RestClient.builder" src/main/javaAny direct client creation fails the review, analogous to forbidding DriverManager.getConnection() in favor of a managed DataSource.
Conclusion
Spring Boot 4.1's InetAddressFilter fills a real gap: developers have long parameterized SQL, file paths, and deserialization, but treated outbound HTTP as harmless. Any URL originating from user input, Excel uploads, webhook configs, third-party callbacks, editable DB fields, or open-platform parameters must be treated as untrusted. The recommended order is now: when creating the RestClient, define allowed address ranges, timeouts, redirect policy, response size limits, and accepted protocols — before writing any business logic. Outbound HTTP is a capability that deserves the same rigor as inbound request handling.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Tech Enthusiast
Sharing computer programming language knowledge, focusing on Java fundamentals, data structures, related tools, Spring Cloud, IntelliJ IDEA... Book giveaways, red‑packet rewards and other perks await!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
