Java

Stop SSRF at the Source: A Practical Java Playbook with Spring Boot 4.1's InetAddressFilter

A production playbook to harden outbound HTTP in Java using Spring Boot 4.1's InetAddressFilter, safe DNS, and egress controls.

Rizky Romadon//18 min read
Share Article

Sources

Article Quality

Structured for practical reading: clear sections, useful depth, and enough context to revisit later.

Reviewed
18 min read
49 sections
Sources included
FAQ ready
Practical examples

Outbound HTTP from Java services is a quiet risk surface. A single "fetch this URL" feature, such as webhook delivery, image proxying, OpenID metadata fetch, or callback verification, can turn your service into a network pivot. The problem is that many teams rely on library defaults that may follow redirects, resolve attacker-controlled hostnames, and connect to internal IPs.

That is the shape of Server-Side Request Forgery, usually shortened to SSRF.

Spring Boot 4.1 adds a small but useful primitive: InetAddressFilter. It gives Java teams a standard way to restrict where HTTP clients are allowed to connect. Used well, it turns a soft risk into a controlled capability with testable boundaries.

This article is a practical playbook for Java teams: how to think, decide, implement, review, and operate safer outbound HTTP using Spring Boot 4.1, Java HTTP clients, DNS discipline, and egress controls. The goal is to make SSRF prevention boring and automatable, not a yearly incident.

Spring Boot 4.1 documents InetAddressFilter as an SSRF mitigation building block for HTTP clients. I will use that as the starting point and focus on production engineering guidance.

TL;DR

  • Treat outbound HTTP like an ingress control problem. Define explicit allowlists, deny private and metadata IPs, and block redirects across trust boundaries.
  • In Spring Boot 4.1, attach an InetAddressFilter to RestClient or WebClient via HTTP client settings. Allow only intended addresses and fail closed on mismatch.
  • Standardize DNS and redirect policy: resolve intentionally, reject redirects that cross your filter, and use an explicit egress proxy where required.
  • Build a network contract per service: allowed hosts, CIDRs, ports, protocols, redirects, and operational owners.
  • Operate with timeouts, circuit breakers, backoff, runtime metrics, and auditable allowlist changes.
  • Use the OWASP SSRF prevention guidance as a policy backbone; this playbook translates the idea into Java and Spring Boot implementation details.

Why It Matters

SSRF is not a theoretical footnote. It is a predictable path to internal systems, metadata endpoints, service discovery, admin panels, and cloud credentials.

Outbound HTTP without guardrails lets an attacker's input become your network scanner. Production incidents here are rarely clever bugs. They are often default behaviors: redirects allowed, DNS trusted too much, private ranges reachable, and metadata endpoints exposed.

The fix is establishing decision boundaries around name resolution, redirection, and address allowlisting, then enforcing them in code and configuration. Spring Boot 4.1's InetAddressFilter helps create that boundary with less custom plumbing.

If you already care about production readiness, this sits beside the habits in Java and Spring Boot Production Checklist and Practical Error Handling in Spring Boot for Reliable Production APIs. It is the same philosophy: make risky behavior explicit before production teaches the lesson for you.

The Practical Problem Engineers Need To Solve

You often need outbound calls. Payments, OAuth discovery, webhooks, file fetchers, notification providers, AI APIs, and third-party integrations all depend on them.

The goal is not to block outbound HTTP entirely. The goal is to allow intended destinations while preventing:

  • Access to internal or private ranges such as 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, and fc00::/7.
  • Access to metadata and link-local ranges such as 169.254.169.254, 169.254.0.0/16, and fe80::/10.
  • DNS rebinding, where a hostname appears harmless but resolves to an internal address later.
  • Redirect chains that silently move a request from an allowed host to an internal host.
  • Library defaults that connect before your application has applied policy.

Outbound HTTP needs a firewall in code:

  • Resolve DNS using known rules.
  • Check IPs against allow and deny logic before connecting.
  • Apply the same rules after every redirect.
  • Emit useful logs and metrics when a filter blocks a call.

InetAddressFilter and Spring Boot HTTP client settings give you a clean hook for this kind of policy.

Decision Framework: Choose the Right SSRF Boundary

Pick the narrowest control that still lets the feature work. Prefer allowlists and proxies over denylists. Document decisions per service.

Who Supplies the URL?

If the URL is external or untrusted, such as user input or tenant-provided callback URLs, require an allowlist by hostname or CIDR. Deny private and metadata ranges. Block redirects across scheme or host boundaries. Consider routing the feature through an egress proxy.

If the URL is internal or configured by operators, still use a policy. Allow by DNS suffix, fixed hostname, or CIDR. Deny private ranges unless the service is explicitly supposed to call internal services.

How Dynamic Is the Target?

For stable hosts, such as OAuth or payment gateways, use a strong allowlist and resolve the destination deliberately.

For arbitrary hosts, such as link previewers, do not let the main application fetch the internet directly. Use a sandboxed fetcher behind an egress proxy with strict timeout, content-type, and size limits.

What Is the Trust Boundary?

For service-to-service traffic inside a mesh, prefer service discovery over raw URLs. If raw URLs are unavoidable, limit them to an internal DNS suffix and approved ports.

For internet-bound calls, use an egress proxy with policy and logging. Keep InetAddressFilter as an in-process guardrail and developer feedback loop.

If you cannot crisply write an allowlist entry for a new feature, the feature is probably under-specified or risky.

Safe Workflow: From Requirement to Running System

Treat outbound HTTP like a feature with a security contract. Capture targets, implement filters, add tests and metrics, then operationalize rollouts.

  1. Capture the contract.

    Document target hosts, domains, ports, protocols, redirect policy, expected status codes, and timeouts.

  2. Implement guardrails.

    Use InetAddressFilter with Spring Boot 4.1 HTTP client settings. Decide whether redirects are disabled, same-host only, or rechecked after every hop.

  3. Add observability.

    Log blocked destination category, resolved address, and policy name. Avoid logging tokens, full callback payloads, or sensitive query strings.

  4. Operate it.

    Provide configuration for allowlist entries and redirect policy. Route risky features through a controlled egress proxy.

  5. Review changes.

    Every new outbound call should pass a lightweight SSRF checklist during pull request review.

This mirrors the same "calm systems" mindset from Building Calm Backend Systems: reduce surprises by turning implicit behavior into explicit contracts.

Implementation Steps with Spring Boot 4.1

Attach InetAddressFilter to your HTTP clients, deny private ranges by default, and explicitly allow only what you intend.

1. Start with a Baseline Configuration

Tight timeouts, conservative redirects, and a service-specific outbound policy are the first layer.

# application.yml
spring:
  http:
    clients:
      connect-timeout: 2s
      read-timeout: 5s
      redirects: dont-follow
  outbound:
    allowed-hosts:
      - "api.stripe.com"
      - "login.microsoftonline.com"
    block-private: true
    block-metadata: true

The exact property names may vary by Spring Boot version and client choice, so treat this as a configuration pattern. The important part is that HTTP behavior is declared and reviewable.

2. Create a Reusable InetAddressFilter

Deny private and metadata ranges, then allow only configured hosts or CIDRs.

package com.example.outbound;
 
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.http.client.InetAddressFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
 
@Configuration
public class OutboundFilterConfig {
 
  @Value("${spring.outbound.block-private:true}")
  boolean blockPrivate;
 
  @Value("${spring.outbound.block-metadata:true}")
  boolean blockMetadata;
 
  @Value("#{'${spring.outbound.allowed-hosts:}'.split(',')}")
  List<String> allowedHosts;
 
  private InetAddressFilter hostAllowlist() {
    List<InetAddressFilter> hostFilters = allowedHosts.stream()
        .map(String::trim)
        .filter(host -> !host.isEmpty())
        .map(host -> {
          try {
            InetAddress[] addresses = InetAddress.getAllByName(host);
            String[] resolvedAddresses = Arrays.stream(addresses)
                .map(InetAddress::getHostAddress)
                .toArray(String[]::new);
 
            return InetAddressFilter.of(resolvedAddresses);
          } catch (UnknownHostException exception) {
            throw new IllegalStateException("Failed to resolve allowed host: " + host, exception);
          }
        })
        .collect(Collectors.toList());
 
    return hostFilters.isEmpty() ? InetAddressFilter.none() : InetAddressFilter.of(hostFilters);
  }
 
  @Bean
  public InetAddressFilter outboundInetFilter() {
    InetAddressFilter base = hostAllowlist();
 
    if (blockPrivate) {
      base = base.andNot(InetAddressFilter.internalAddresses());
    }
 
    if (blockMetadata) {
      InetAddressFilter linkLocal = InetAddressFilter.of("169.254.0.0/16", "fe80::/10");
      base = base.andNot(linkLocal);
      base = base.andNot(InetAddressFilter.multicast());
    }
 
    return base;
  }
}

InetAddressFilter provides convenience methods such as internalAddresses(), multicast(), of(), and combinators like andNot() so teams can build precise policies instead of scattering IP checks across services.

3. Wire RestClient with HTTP Client Settings

The goal is to make the safe path the default path. Teams should not create outbound clients by hand in random feature code.

package com.example.outbound;
 
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.http.client.HttpClientSettings;
import org.springframework.boot.http.client.InetAddressFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
 
@Configuration
public class HttpClientConfig {
 
  @Bean
  RestClient restClient(InetAddressFilter outboundInetFilter) {
    HttpClientSettings settings = HttpClientSettings.builder()
        .inetAddressFilter(outboundInetFilter)
        .build();
 
    ClientHttpRequestFactory factory =
        ClientHttpRequestFactoryBuilder.httpComponents().build(settings);
 
    return RestClient.builder()
        .requestFactory(factory)
        .build();
  }
}

The same idea applies to WebClient using the supported connector configuration for your Spring Boot version.

4. Treat Redirects as a New Boundary

Prefer dont-follow unless redirects are required.

If the feature must follow redirects:

  • Re-resolve the redirect host.
  • Re-check the resolved address with the same filter.
  • Block HTTPS to HTTP downgrades.
  • Block redirects to different hosts unless explicitly allowed.
  • Keep a small maximum redirect count.

Redirects are not a minor HTTP detail. They are another destination decision.

5. Use an Egress Proxy for Internet-Bound Features

For higher-risk features, put the service behind an egress proxy that enforces the same rules at the network layer. The in-process InetAddressFilter then becomes a second boundary and a better developer experience.

This is especially useful when many services need the same policy, audit trail, and emergency allowlist process.

Concrete Example: OAuth Discovery with Guardrails

OpenID Connect discovery URLs are often tenant-provided, which makes them easy to underestimate.

Contract

  • Only allow approved identity provider hosts.
  • Require https.
  • Deny redirects to unapproved hosts.
  • Deny private, metadata, and link-local IP ranges.
  • Use short timeouts.

Implementation Flow

  1. Validate the URL before creating a request.
  2. Require the scheme to be https.
  3. Require the host to match the configured allowlist.
  4. Resolve and filter the destination before connect.
  5. Disable redirects or re-check every redirect target.
  6. Parse the response with a size limit.
URI discoveryUri = URI.create(inputDiscoveryUrl);
 
if (!"https".equalsIgnoreCase(discoveryUri.getScheme())) {
  throw new IllegalArgumentException("Discovery URL must use HTTPS");
}
 
if (!allowedHosts.contains(discoveryUri.getHost())) {
  throw new IllegalArgumentException("Discovery host is not allowed");
}
 
String body = restClient.get()
    .uri(discoveryUri)
    .retrieve()
    .body(String.class);

This validation does not replace InetAddressFilter. It protects the feature at the semantic layer. The filter protects the network layer.

Tests

Add tests for:

  • http://localhost
  • http://127.0.0.1
  • http://169.254.169.254
  • A hostname resolving to 10.0.0.7
  • A valid host returning a redirect to a blocked address
  • A redirect from https to http

Decision Boundaries and Patterns

Pick patterns per use case and standardize them.

Use CaseRecommended PatternWhat To Avoid
Webhook deliveryAllowlisted vendor hosts, no redirects, strict TLS, retries with backoffArbitrary callback URLs without filtering
Image proxy or URL previewSandboxed fetcher, egress proxy, content-type and size limitsLetting the main app fetch arbitrary URLs
Payments and identityStrict allowlist, short timeouts, reliable retriesWildcard domains and automatic redirects
Service-to-service callsService discovery or internal DNS suffix with approved portsRaw URLs mixed into feature logic
Admin integrationsStatic configuration, approval workflow, audit logsTenant-editable internal endpoints

This is where DevOps Release Habits for Small Teams becomes relevant. A good policy still needs rollout discipline, telemetry, and a path to revert safely.

Review Checklist for Code and Design

Make this a standard pull request ritual. The point is not bureaucracy. The point is to catch unsafe defaults while changes are still cheap.

Inputs

  • Is the URL attacker-controlled, tenant-controlled, or operator-controlled?
  • Is the scheme validated?
  • Is the host validated?
  • Are unexpected ports blocked?

Resolution and Redirects

  • Does the client vet resolved addresses before connecting?
  • Are redirects disabled or rechecked against the same filter?
  • Is https to http downgrade blocked?
  • Is the maximum redirect count small?

Filter and Policy

  • Is InetAddressFilter attached to the client actually used by the feature?
  • Are private, metadata, link-local, multicast, and IPv6 ranges handled?
  • Are allowed hosts or CIDRs justified?
  • Is there a test for blocked destinations?

Observability

  • Are blocked attempts logged without leaking secrets?
  • Are metrics emitted for allowed and blocked calls?
  • Are labels bounded, such as destination category or host, not full URL?

Operations

  • Is there a runtime configuration path for allowlist changes?
  • Is the change auditable?
  • Is an egress proxy used for internet-bound traffic?
  • Is there a rollback plan?

Security Considerations

SSRF defense is layered. No single control is sufficient.

Apply an allowlist first. Deny private and metadata ranges regardless. InetAddressFilter helps implement the in-process boundary, but network policy and proxy enforcement still matter.

Enforce TLS correctly. Hostname verification must validate the original host, not a pinned IP address. Do not break certificate validation while trying to solve DNS behavior.

Sanitize redirects. A redirect target is a new destination and must pass the same checks.

Treat responses as untrusted. For fetchers, enforce content-type, content length, decompression limits, and parsing limits.

Review third-party library defaults. Redirects, decompression, DNS behavior, and connection pooling can vary across clients.

Align the policy with OWASP SSRF prevention guidance:

  • Prefer allowlists.
  • Block link-local and metadata IPs.
  • Use network-layer controls.
  • Avoid trusting DNS alone.
  • Harden cloud metadata access where possible.

Performance and Operations Considerations

Guardrails should not hurt latency if they are pragmatic.

DNS Resolution

Cache positive results with short TTLs and respect DNS behavior. Do not pin across many requests unless you understand the CDN or vendor implications.

Track resolution time and cache hit rates. DNS issues should be visible, not hidden inside generic HTTP latency.

Timeouts and Backoff

Use tight connect and read timeouts. Combine retries with jittered backoff and circuit breakers.

Do not retry blocked destinations. A policy block is not a transient network failure.

Observability

Emit cardinality-bounded labels:

  • Destination category
  • Host
  • Policy name
  • Block reason
  • Client name

Avoid full URLs in metric labels. They create cardinality problems and may leak secrets.

Rollouts

Start in observe-only mode if a service has many existing integrations. Log what would be blocked, review the results, then move to fail-closed mode.

Maintain a rapid but audited allowlist configuration path. This is important because vendors change infrastructure, incidents happen, and teams need a safe operational escape hatch.

Common Mistakes

Defaults are often friendly to attackers.

  • Validating only the scheme and host, not the resolved IP.
  • Allowing redirects without rechecking the target.
  • Forgetting IPv6.
  • Forgetting metadata and link-local ranges.
  • Assuming the HTTP client is secure by default.
  • Mixing wildcard allowlists carelessly.
  • Ignoring IDN and punycode edge cases.
  • Logging full URLs with tokens.
  • Not testing blocked paths.
  • Letting every team create its own outbound HTTP client.

These are simple mistakes, but they become expensive when repeated across services.

Production Readiness Checklist

Verify these before exposing the feature.

AreaCheckStatus
PolicyAllowlist documented with hosts, CIDRs, ports, and owners[ ]
ClientInetAddressFilter attached to all outbound clients[ ]
RedirectsDisabled or re-vetted on each hop[ ]
DNSResolve-then-vet behavior documented and tested[ ]
TLSHostname verification enforced[ ]
OpsMetrics for allowed and blocked calls[ ]
ConfigAllowlist modifiable without deploy and with audit trail[ ]
TestsUnit and integration tests for allow and deny cases[ ]

Code Review Checklist

A compact reviewer pass should answer these questions.

QuestionYes/No
Does the code use the shared safe HTTP client?[ ]
Is InetAddressFilter attached to the client?[ ]
Are private and metadata ranges denied?[ ]
Is there a specific allowlist?[ ]
Are redirects disabled or tightly constrained?[ ]
Do logs and metrics record blocked attempts safely?[ ]
Are timeout and retry rules explicit?[ ]

How To Apply This In a Real Team

Formalize the workflow and automate the checks.

  1. Define a team-wide outbound access policy.

    Keep it short. Link to the OWASP SSRF cheat sheet. Explain allowlists, redirects, private ranges, metadata endpoints, and egress proxy expectations.

  2. Create a shared starter.

    Publish an internal Spring Boot starter or shared module that attaches the default InetAddressFilter and exposes configuration.

  3. Add CI checks.

    Fail builds when feature code creates ad hoc HTTP clients instead of the shared factory.

  4. Maintain an egress registry.

    Each service should declare allowed hosts, ports, redirect behavior, and owner. The registry should be reviewable in pull requests.

  5. Practice incident drills.

    Simulate a redirect to a private IP. Verify it is blocked, logged, alerted, and easy to explain.

If your team uses coding assistants to generate integration code, pair this with How Backend Engineers Should Use AI Coding Agents Without Losing Control. Assistants can produce HTTP code quickly, but the boundary policy still needs to be human-owned.

Realistic Failure Scenarios and Tradeoffs

Expect friction. Design for graceful failure.

A Vendor Changes Infrastructure

A vendor temporarily changes IP ranges and your filter blocks calls.

Mitigation: use runtime-configurable allowlists with audit logs. Keep urgent changes reversible and visible.

DNS-Based Multi-CDN Hosts Behave Differently

Per-request pinning may route traffic to a less optimal point of presence or break geo-aware behavior.

Mitigation: pin only within the request lifecycle, respect TTLs, and monitor latency by destination.

The Egress Proxy Fails

A proxy can become a single point of failure.

Mitigation: health-check proxies, publish policy versions, and keep conservative in-process filtering as a second line of defense.

Partner Redirects Break

Some partners rely on redirects for versioned endpoints.

Mitigation: allow same-host redirects only, add explicit exceptions, and require tests for those exceptions.

FAQ

Why use both an InetAddressFilter and an egress proxy?

Defense in depth. The in-process filter blocks unsafe destinations close to the code path and gives developers immediate feedback. The proxy centralizes policy across many services.

Do not let your main application fetch arbitrary URLs. Use a sandboxed fetcher behind an egress proxy with strict content-type, size, timeout, and redirect limits. Add an InetAddressFilter that denies private and metadata IPs.

Do I need DNS pinning?

It helps prevent mid-flight rebinding. Pin within the lifecycle of a single request: resolve, vet, then connect. Still verify TLS hostnames against the original host.

What about IPv6?

Handle it explicitly. Block private, link-local, and multicast ranges for IPv6 as well as IPv4. InetAddressFilter supports IPv6 literals and CIDRs.

How do redirects fit into SSRF defenses?

Redirects are another resolution step. Disable them when not required. If required, reapply the same filter to each redirect location and block scheme or host boundary changes.

Is Spring Boot 4.1 required?

No, but it is convenient. Spring Boot 4.1 documents InetAddressFilter and HTTP client settings so teams can standardize SSRF mitigation with less glue code. Older stacks can implement equivalent filters or route through a policy-enforcing proxy.

Conclusion

Outbound HTTP should be as deliberate as ingress policy.

By attaching InetAddressFilter in Spring Boot 4.1, denying private and metadata ranges, explicitly allowlisting destinations, and treating redirects as boundary crossings, Java teams can make SSRF prevention simpler and repeatable.

Pair the code with practical operations: metrics, alerts, fast allowlist changes, and an egress registry. Your next step is small: add the shared filter module to one service, run it in observe-only mode, review the telemetry, then move to fail-closed behavior.

FAQ Schema

{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "Why use both an InetAddressFilter and an egress proxy?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Defense in depth. The in-process filter blocks unsafe destinations close to the code path and provides developer feedback, while the proxy centralizes policy across services."
      }
    },
    {
      "@type": "Question",
      "name": "How do I safely handle user-supplied URLs for link previews?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Use a sandboxed fetcher behind an egress proxy with strict limits such as content-type, size, timeout, and redirects. Add an InetAddressFilter that denies private and metadata IPs."
      }
    },
    {
      "@type": "Question",
      "name": "Do I need DNS pinning?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Pin within a single request lifecycle to reduce DNS rebinding risk. Still verify TLS hostnames against the original host and monitor performance impacts."
      }
    },
    {
      "@type": "Question",
      "name": "What about IPv6 in SSRF defenses?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Block private, link-local, and multicast ranges for IPv6 as well as IPv4. InetAddressFilter supports IPv6 literals and CIDRs."
      }
    },
    {
      "@type": "Question",
      "name": "How should redirects be handled?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Disable redirects when not required. If needed, reapply the same filter to each redirect and block changes that cross host or scheme boundaries."
      }
    },
    {
      "@type": "Question",
      "name": "Is Spring Boot 4.1 required for InetAddressFilter?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "No, but Spring Boot 4.1 documents InetAddressFilter and HTTP client settings, which simplifies standardizing SSRF mitigations. Older stacks can implement equivalent filters or rely on egress proxies."
      }
    }
  ]
}

Related Posts