Skip to content

Changelog

All notable changes to ssrf-guard are recorded here.

The format follows Keep a Changelog and the project adheres to Semantic Versioning.

[Unreleased]

[3.1.0] — LLM core extraction, LangChain4j, WebClient DNS gap, GraalVM hints

Added

  • ssrf-guard-llm — new framework-agnostic core module holding the JSON walking + URL extraction + policy validation that every LLM tool adapter shares. Exposes ToolInputGuard (interface) and JsonToolInputGuard (default impl). Holds the ~200 lines of logic that previously lived inside SsrfGuardedToolCallback.
  • ssrf-guard-langchain4j — new adapter module wrapping LangChain4j's ToolExecutor. Closes the same LLM-agent SSRF surface as ssrf-guard-springai but for the LangChain4j ecosystem (the other major Java LLM framework). Auto-wrap via BeanPostProcessor for Spring Boot users; SsrfGuardedToolExecutors.wrap(...) helpers for non-Spring / programmatic users.
  • GraalVM native-image friendliness. ssrf-guard-llm registers a RuntimeHintsRegistrar (via META-INF/spring/aot.factories) so Spring Boot's AOT processor learns about the reflective surface this library uses at runtime: the new SsrfBlockPayload record (Jackson-serialised on every block) and the BlockReason enum (Jackson-touched for its label). Adapter modules (-springai, -langchain4j, all Spring autoconfigs) get free AOT coverage from Spring Boot 3 — they only host @Bean factory methods and a BeanPostProcessor, both of which the AOT processor already handles.

Changed

  • ssrf-guard-springai refactored to a thin adapter (~30 lines). Delegates to JsonToolInputGuard from the new -llm module. Public API unchanged — every constructor and method on SsrfGuardedToolCallback keeps the same shape. v3.0.x consumers see no API change; they just pick up ssrf-guard-llm transitively.
  • Error-payload shape stabilised. The JSON object an LLM sees on an SSRF block is now backed by the typed SsrfBlockPayload record ({error, reason, url, message, guidance}). Wire-compatible with v3.0.x — same field names, same values. Replaces the previous Map.of(...) form whose JDK-private ImmutableCollections.MapN backing types AOT couldn't introspect. If you scripted around the wire shape you don't need to change anything; substring assertions in existing tests keep passing.

Fixed

  • WebClient DNS-time defense gap closed. v3.0.x's ssrf-guard-webclient only ran the URL-time filter — a host that passed the whitelist could still resolve to a private IP at DNS time (the classic DNS-rebinding-to-metadata attack). The new SsrfGuardReactorAddressResolverGroup plugs into reactor-netty's AddressResolverGroup and filters resolved IPs against the same private-IP ranges the RestClient module checks at the Apache HttpClient DnsResolver step. WebFlux apps now get the same two-layer defense (URL + DNS) the blocking RestClient apps already had. Gated on reactor-netty being on the classpath — non-Netty WebFlux backends (Jetty Reactive, Helidon) still get the URL-time filter and just skip the connector swap.

Migration

Drop in v3.1.0 — no consumer code changes. The meta kr.devslab:ssrf-guard:3.1.0 still transitively pulls in -core, -httpclient5, -restclient like v3.0.x. New modules (-llm, -langchain4j) are opt-in; consumers who don't add their coordinate don't pull the new jars.

If you previously caught SecurityException you still match SsrfGuardException (it's still a SecurityException subclass). Catch SsrfGuardException to read the structured e.reason().

For native-image consumers: add kr.devslab:ssrf-guard-llm:3.1.0 (or any module that depends on it transitively) and the AOT processor will pick up the registered hints automatically.

[3.0.1] — Fix metrics bean classpath gate

Fixed

  • ClassNotFoundException: io.micrometer.core.instrument.MeterRegistry when consumers don't have micrometer-core on the classpath. Affected every Spring autoconfig module — -restclient, -resttemplate (via -restclient), -webclient, -feign, -springai — because the metrics bean factory method declared ObjectProvider<MeterRegistry> as a parameter, and the JVM resolves parameter types at class load time even when ObjectProvider would otherwise handle the missing bean gracefully.
  • The fix moves the Micrometer-backed metrics bean into a static inner @Configuration class gated by @ConditionalOnClass(name = "io.micrometer.core.instrument.MeterRegistry") (string form, so Spring's ASM-based condition evaluator inspects the annotation without JVM-loading Micrometer). The outer autoconfig declares a fallback @Bean returning NoOpSsrfGuardMetrics under @ConditionalOnMissingBean(SsrfGuardMetrics.class).

Migration

Drop in v3.0.1 — no consumer code changes. If you previously worked around the v3.0.0 bug by adding io.micrometer:micrometer-core to your build only because of this error (not because you actually wanted metrics), the dep can come out now.

[3.0.0] — Multi-module + LLM agent SSRF defense

The v2.0.0 starter was a single jar that only worked with Spring's RestClient. v3.0.0 splits the codebase along client boundaries, adds support for every common JVM HTTP stack, and ships a Spring AI Tool wrapper that closes the SSRF surface LLM agents have been introducing for the last two years.

Added — new modules (opt-in)

Module Use case
ssrf-guard-core Policy / NetUtil / Micrometer metrics interface — no Spring dependency
ssrf-guard-httpclient5 Apache HttpClient 5 DnsResolver + RedirectStrategy (TOCTOU closure)
ssrf-guard-restclient Spring 6.1+ RestClient autoconfig (the v2.0.0 surface, now its own module)
ssrf-guard-resttemplate NEW — Spring RestTemplate autoconfig for the enterprise/legacy crowd
ssrf-guard-webclient NEW — Spring WebFlux WebClient ExchangeFilterFunction + autoconfig
ssrf-guard-feign NEW — Spring Cloud OpenFeign RequestInterceptor + autoconfig
ssrf-guard-springai NEW — Spring AI ToolCallback wrapper that validates URL-shaped tool arguments before LLM-driven execution. The hot SSRF surface in 2025+
ssrf-guard-jdkhttp NEWjava.net.http.HttpClient wrapper (no Spring, JDK 11+)
ssrf-guard-okhttp NEW — OkHttp Interceptor + Dns (no Spring)
ssrf-guard Meta artifact — bundles -core + -httpclient5 + -restclient for v2.0.0 back-compat

Added — defense-in-depth hardening

  • IP-literal host rejection (ssrf.guard.reject-ip-literal-hosts=true default). Any URL whose host parses as an IP literal in any form — dotted decimal (127.0.0.1), bare decimal (2130706433), hex (0x7f000001), octal (0177.0.0.1), partial (127.1), IPv6 ([::1]) — is rejected at the URL-time check, before DNS. Closes the obfuscated-IP bypass class.
  • Userinfo rejection (ssrf.guard.reject-user-info=true default). URLs of the form https://user:pass@host/... are rejected — known SSRF bypass vector and credential-leak risk.
  • IPv4-mapped IPv6 + 6to4 unmapping. ::ffff:10.0.0.5 and 2002:0a00:: (the 6to4 form wrapping 10.0.0.0/8) are now correctly classified as private, not "public IPv6 that happens to embed an internal v4". Java's isLoopbackAddress() misses these.

Added — observability

  • Micrometer metrics, auto-wired when a MeterRegistry bean is on the classpath:
    ssrf_guard_blocked_total{reason="blocked_private_ip", scheme="http"} 42
    ssrf_guard_allowed_total{scheme="https"} 13042
    
  • Structured WARN logs on every block: ssrf-guard: <message> (reason=blocked_private_ip, scheme=http, host=169.254.169.254).

Changed — BREAKING

  • Package renames. Types moved out of the catch-all kr.devslab.ssrfguard.security package into their respective modules. The ssrf-guard meta artifact re-exports them, so import kr.devslab.ssrfguard.* consumers may need to update imports:
v2.0.0 v3.0.0
kr.devslab.ssrfguard.autoconfigure.SsrfGuardAutoConfiguration kr.devslab.ssrfguard.restclient.SsrfGuardRestClientAutoConfiguration
kr.devslab.ssrfguard.autoconfigure.SsrfGuardProperties kr.devslab.ssrfguard.core.SsrfGuardProperties
kr.devslab.ssrfguard.security.SsrfGuardInterceptor kr.devslab.ssrfguard.restclient.SsrfGuardClientHttpRequestInterceptor
kr.devslab.ssrfguard.security.SafeDnsResolver kr.devslab.ssrfguard.httpclient5.SafeDnsResolver
kr.devslab.ssrfguard.security.SafeRedirectStrategy kr.devslab.ssrfguard.httpclient5.SafeRedirectStrategy
kr.devslab.ssrfguard.security.NetUtil kr.devslab.ssrfguard.core.NetUtil
- SecurityExceptionSsrfGuardException. All rejection paths now throw SsrfGuardException (still a subclass of SecurityException, so v2.0.0 catch blocks keep working). The exception carries a BlockReason enum tag for metrics / logging.
- New properties. ssrf.guard.reject-ip-literal-hosts and ssrf.guard.reject-user-info default to true — turning them off restores v2.0.0 behaviour on those two checks.

Migration

For most consumers, update the version and rebuild — that's it:

<dependency>
    <groupId>kr.devslab</groupId>
    <artifactId>ssrf-guard</artifactId>
    <version>3.0.0</version>
</dependency>

The ssrf-guard meta artifact transitively pulls in -core, -httpclient5, and -restclient, which together provide the entire v2.0.0 surface.

If you use a different HTTP client, pick the matching module:

<!-- RestTemplate -->
<dependency>
    <groupId>kr.devslab</groupId>
    <artifactId>ssrf-guard-resttemplate</artifactId>
    <version>3.0.0</version>
</dependency>

<!-- WebClient -->
<dependency>
    <groupId>kr.devslab</groupId>
    <artifactId>ssrf-guard-webclient</artifactId>
    <version>3.0.0</version>
</dependency>

<!-- Spring AI tool calls -->
<dependency>
    <groupId>kr.devslab</groupId>
    <artifactId>ssrf-guard-springai</artifactId>
    <version>3.0.0</version>
</dependency>

If your code catches SecurityException from outbound calls, it still works — SsrfGuardException extends SecurityException. If you want the structured tag, catch SsrfGuardException and inspect e.reason().

[2.0.0] — Rebrand to kr.devslab:ssrf-guard

Changed

  • BREAKING — coordinate changed. From com.devs.lab:ssrf-guard-spring-boot-starter to kr.devslab:ssrf-guard. The legacy artifact was never published to Maven Central, so v2.0.0 is the first proper Central release.
  • BREAKING — package renamed. devs.lab.ssrf.*kr.devslab.ssrfguard.*:
Old New
devs.lab.ssrf.config.SsrfGuardAutoConfiguration kr.devslab.ssrfguard.autoconfigure.SsrfGuardAutoConfiguration
devs.lab.ssrf.security.SsrfGuardProperties kr.devslab.ssrfguard.autoconfigure.SsrfGuardProperties
devs.lab.ssrf.security.SsrfGuardInterceptor kr.devslab.ssrfguard.security.SsrfGuardInterceptor
devs.lab.ssrf.security.SafeDnsResolver kr.devslab.ssrfguard.security.SafeDnsResolver
devs.lab.ssrf.security.SafeRedirectStrategy kr.devslab.ssrfguard.security.SafeRedirectStrategy
devs.lab.ssrf.security.NetUtil kr.devslab.ssrfguard.security.NetUtil
  • BREAKING — SsrfGuardApplication removed. The empty @SpringBootApplication was vestigial scaffolding from the original Spring Initializr template; a starter library has no business carrying a main class.
  • Build system: Maven → Gradle 8.10 with Vanniktech maven-publish 0.30.0. Same convention as easy-paging-spring-boot-starter and api-log.
  • Release flow: semantic-release → tag-triggered Gradle publish. A git tag matching v[0-9]+.[0-9]+.[0-9]+ runs the release workflow, which builds + signs + uploads to Sonatype Central Portal and creates a GitHub Release in one step.

Added

  • CI workflow (.github/workflows/ci.yml) — runs ./gradlew build jacocoTestReport on every push to main and on every PR, uploads coverage to Codecov.
  • Docs site at https://ssrf-guard.devslab.kr/ — installation, quickstart, security model, configuration reference. Built with mkdocs-material + i18n (English + Korean).
  • Bilingual README (README.md / README.ko.md).
  • Full test coverage of every documented defense:
  • NetUtilTest — whitelist matching (exact + suffix), IDN normalisation, private-IP classification across IPv4 (loopback, RFC-1918, link-local incl. AWS metadata, CGNAT, benchmark, broadcast) and IPv6 (ULA, link-local).
  • SafeDnsResolverTest — whitelist gate + private-IP filter, including the "filtered everything" path.
  • SsrfGuardInterceptorTest — scheme/host/port accept/reject matrix, suffix label-boundary lookalike (the classic badexample.com bypass).
  • SsrfGuardAutoConfigurationTest — every public bean of the auto-config is registered when enabled, and none are when ssrf.guard.enabled=false.
  • SsrfGuardIntegrationTest — real HTTP through MockWebServer, end-to-end through the four-layer defense.

Migration

Update your dependency coordinate and any direct imports:

<!-- v1.x (never on Maven Central) -->
<dependency>
    <groupId>com.devs.lab</groupId>
    <artifactId>ssrf-guard-spring-boot-starter</artifactId>
    <version>1.1.0</version>
</dependency>

<!-- v2.0.0 -->
<dependency>
    <groupId>kr.devslab</groupId>
    <artifactId>ssrf-guard</artifactId>
    <version>2.0.0</version>
</dependency>

application.yml keys are unchanged — ssrf.guard.* works identically.

Direct imports of the security types? Replace devs.lab.ssrf with kr.devslab.ssrfguard and split between kr.devslab.ssrfguard.autoconfigure (properties + auto-config) and kr.devslab.ssrfguard.security (interceptor + resolver + redirect + NetUtil).

[1.1.0] — 2025-09-23

semantic-release rollup of pre-v2 work. Tagged but never published to Maven Central.

  • README + releaser templates touched up

[1.0.0] — 2025-09-23

Initial public release. Tagged but never published to Maven Central.

  • First cut of the SSRF starter under com.devs.lab:ssrf-guard-spring-boot-starter