Spring Boot 4.1's InetAddressFilter Finally Blocks SSRF in URL Preview APIs
The article demonstrates how Spring Boot 4.1's new InetAddressFilter.externalAddresses() simplifies SSRF protection for user-supplied URLs, replacing manual IP checks with a centralized HTTP client filter, while also covering URL validation, redirect handling, timeouts, response size limits, and testing strategies to secure link preview and similar features.
Vulnerable Link Preview Implementation
A typical link preview endpoint fetches a user-provided URL, extracts HTML metadata, and returns a preview card. The naive implementation uses Spring's RestClient directly with the user-supplied URL:
@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);
}
}This allows attackers to supply internal addresses like http://127.0.0.1:8080/xxx or http://10.0.0.10/xxx, causing the server to scan internal networks — a classic SSRF (Server-Side Request Forgery) vulnerability.
Evolution of Manual IP Blocking
The author traces common inadequate fixes:
String checks for localhost — bypassed by 127.0.0.1.
Adding 127.0.0.1 — still misses other loopback ranges.
Expanding to private CIDR blocks: 10.x.x.x, 172.16.x.x, 192.168.x.x, IPv6 equivalents.
DNS-based resolution check using InetAddress.getByName() and isLoopbackAddress() / isSiteLocalAddress().
The DNS approach still fails because it only validates the initial hostname; a malicious DNS could resolve internal.example.test to 192.168.1.20 after the check passes.
Centralizing Protection with Spring Boot 4.1's InetAddressFilter
Spring Boot 4.1 introduces InetAddressFilter to enforce allowed destination addresses at the HTTP client level. The key insight: InetAddressFilter defines allowed addresses, not blocked ones. InetAddressFilter.externalAddresses() permits only public internet addresses, automatically rejecting loopback, private ranges, and other internal networks.
@Configuration(proxyBeanMethods = false)
public class HttpClientSecurityConfig {
@Bean
public InetAddressFilter httpClientInetAddressFilter() {
return InetAddressFilter.externalAddresses();
}
}Critical requirement: the RestClient must be built from Spring Boot's auto-configured RestClient.Builder. Manually creating clients via RestClient.create() or RestClient.builder().build() bypasses the filter.
Two-Layer Defense: URL Validation + Network Filter
The author retains a lightweight ExternalUrlValidator at the business layer to enforce URI syntax rules (HTTP/HTTPS only, valid host, no userInfo), while delegating network-level IP filtering to InetAddressFilter at the HTTP client layer.
@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;
}
}The service combines both:
@Service
public class LinkPreviewService {
private final RestClient restClient;
private final ExternalUrlValidator urlValidator;
public LinkPreviewService(RestClient.Builder builder, ExternalUrlValidator urlValidator) {
this.restClient = builder.build();
this.urlValidator = urlValidator;
}
public String fetch(String value) {
URI uri = urlValidator.validate(value);
return restClient.get()
.uri(uri)
.retrieve()
.body(String.class);
}
}Filtered requests throw FilteredHostException, which a global handler converts to a generic 403 response without leaking internal IP details.
Redirect Handling and Timeouts
Automatic redirect following is dangerous: a public URL can redirect to an internal address. The author disables auto-redirects via configuration:
spring:
http:
clients:
connect-timeout: 2s
read-timeout: 3s
redirects: dont-followIf redirects are needed, they recommend a custom implementation with a max of 3 hops, protocol restrictions (http→http, http→https, https→https), and re-validation of each new location through the same security pipeline.
Response Size and Content-Type Limits
To prevent memory exhaustion from large responses, the final downloader uses exchange() with a 512 KB limit, validates HTTP 2xx status, and ensures Content-Type is HTML-compatible:
@Service
public class RemotePageDownloader {
private static final int MAX_BODY_SIZE = 512 * 1024;
private final RestClient restClient;
private final ExternalUrlValidator validator;
public RemotePageDownloader(RestClient.Builder builder, ExternalUrlValidator validator) {
this.restClient = builder.build();
this.validator = validator;
}
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);
}
});
}
}Testing and Governance
Unit tests verify the filter blocks loopback ( 127.0.0.1) and private addresses ( 192.168.1.10). More importantly, the author adds repository-level checks to prevent bypassing the auto-configured builder:
grep -R "RestClient.create" src/main/java
grep -R "RestClient.builder" src/main/javaA code review rule is established: business code must not create HTTP clients directly; all external requests must use the Spring-provided RestClient.Builder — analogous to using a shared DataSource instead of DriverManager.getConnection().
Conclusion
Spring Boot 4.1's InetAddressFilter fills a real gap: previously every project hand-rolled internal IP detection. Now a single bean configures network egress policy, supports CIDR and filter composition for stricter whitelists, and shifts SSRF protection left — applied when the RestClient is created, not as an afterthought. The author now defines address scope, timeouts, redirects, response size, and allowed protocols upfront, treating outbound HTTP requests as a permission that must be explicitly scoped.
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.
LuTiao Programming
LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.
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.
