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.

LuTiao Programming
LuTiao Programming
LuTiao Programming
Spring Boot 4.1's InetAddressFilter Finally Blocks SSRF in URL Preview APIs

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-follow

If 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/java

A 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaBackend DevelopmentSpring BootSecurityRestClientSSRFInetAddressFilterURL Preview
LuTiao Programming
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.