Skip to content

v3.1 (2/N): WebClient DNS-time defense via reactor-netty AddressResolverGroup#7

Merged
jlc488 merged 1 commit into
mainfrom
v3.1.0/webclient-dns-guard
May 23, 2026
Merged

v3.1 (2/N): WebClient DNS-time defense via reactor-netty AddressResolverGroup#7
jlc488 merged 1 commit into
mainfrom
v3.1.0/webclient-dns-guard

Conversation

@jlc488
Copy link
Copy Markdown
Collaborator

@jlc488 jlc488 commented May 23, 2026

Summary

Closes the WebClient module's DNS-time defense gap. Before this PR, ssrf-guard-webclient only ran the URL-time filter (ExchangeFilterFunction) — a host that passed the whitelist could still resolve to a private IP and the request would proceed. This PR adds the same private-IP filter the RestClient module already had at Apache HttpClient's DnsResolver layer, but for reactor-netty's AddressResolverGroup.

What's added

SsrfGuardReactorAddressResolverGroup

Wraps Netty's DefaultAddressResolverGroup. Filters resolved InetSocketAddress instances using the same NetUtil.isPrivateOrLocal predicate the other modules use.

Two paths covered:

  • doResolve (single result) — fails with UnknownHostException if the single resolved IP is private.
  • doResolveAll (multi-A) — drops private IPs from the result; fails only if every IP is private.

Behaviour matrix tested directly against a stub upstream:

Resolved IPs block-private-networks Outcome
All public n/a Pass
All private true (default) UnknownHostException
All private false Pass
Mixed true Public kept, privates filtered

Autoconfig changes

SsrfGuardWebClientAutoConfiguration now publishes:

  • SsrfGuardReactorAddressResolverGroup bean
  • ReactorClientHttpConnector bean built on HttpClient.create().resolver(resolverGroup)

The existing ssrfWebClientCustomizer injects the connector via ObjectProvider<ReactorClientHttpConnector> and calls .clientConnector(connector) on the builder — additive to the existing .filter(...) call.

Both wired with @ConditionalOnMissingBean:

  • Consumers with their own ReactorClientHttpConnector (custom pool, proxy, mTLS) win
  • Non-Netty WebFlux backends (Jetty Reactive, Helidon) skip the connector wiring (the ReactorNettyConnectorConfiguration inner class is gated by @ConditionalOnClass(HttpClient.class))

Either way, the URL-time filter keeps working.

Test plan

  • 6 new resolver tests against stub upstream — every cell of the behaviour matrix above
  • 7 existing webclient tests still pass after autoconfig changes
  • Local full build green across all 11 modules
  • CI green on this PR

Non-breaking

SsrfGuardExchangeFilterFunction and SsrfGuardedToolCallback signatures unchanged. The new connector bean only activates when reactor-netty is present and no other ReactorClientHttpConnector bean is in the context. Consumers who never call .resolver(...) themselves still benefit immediately on upgrade.

What's NOT in this PR (next phase)

  • GraalVM Native Image hints
  • Tag v3.1.0 + release + demo bumps

v3.0.x's ssrf-guard-webclient only ran URL-time validation via the
ExchangeFilterFunction. A host that passed the whitelist could still
resolve to a private IP at DNS time — the classic
DNS-rebinding-to-metadata attack the RestClient module already blocked
through Apache HttpClient's DnsResolver step.

This closes the gap. SsrfGuardReactorAddressResolverGroup extends
Netty's AddressResolverGroup, delegates DNS lookup to the JDK default,
then filters resolved InetSocketAddresses against the same
NetUtil.isPrivateOrLocal ranges used elsewhere in the project.

Wiring

  SsrfGuardWebClientAutoConfiguration now publishes a
  ReactorClientHttpConnector bean built on
  HttpClient.create().resolver(resolverGroup). The existing
  ssrfWebClientCustomizer picks it up via ObjectProvider and calls
  .clientConnector(connector) on the builder — additive to the
  filter().

  Both gated:
    - The connector wiring lives in ReactorNettyConnectorConfiguration
      (static inner @configuration) gated by
      @ConditionalOnClass(HttpClient.class). Non-Netty WebFlux backends
      (Jetty Reactive, Helidon) keep the URL-time filter, skip the
      connector swap.
    - @ConditionalOnMissingBean(ReactorClientHttpConnector.class) so
      consumers with their own connector (custom pool, proxy, mTLS)
      win. Those consumers can still attach the resolver themselves
      via the SsrfGuardReactorAddressResolverGroup bean.

Behaviour matrix

  | Resolved IPs | block-private-networks | Outcome |
  |---|---|---|
  | All public | n/a | Pass through |
  | All private | true (default) | UnknownHostException |
  | All private | false | Pass through |
  | Mixed | true | Public ones kept, privates filtered |

Tests

  6 new tests in SsrfGuardReactorAddressResolverGroupTest covering
  every behaviour-matrix cell against a stub upstream AddressResolverGroup
  (no real DNS) — public IP, loopback, AWS metadata link-local, multi-A
  mixed, all-private, filter-disabled.

  All 13 webclient tests pass (7 existing + 6 new).
@jlc488 jlc488 merged commit bb55093 into main May 23, 2026
1 check passed
jlc488 added a commit that referenced this pull request May 23, 2026
Cleans up the v3.1 baseline before tagging:

Docs (en/ko, symmetric):
  - index.md / .ko.md            — module matrix gets -llm and
                                    -langchain4j rows; webclient row
                                    notes the new URL+DNS coverage.
  - README.md / .ko.md            — same matrix in the repo's front-door.
  - installation.md / .ko.md      — two new install tabs (LangChain4j
                                    tool execution, Custom tool
                                    dispatcher via ssrf-guard-llm).
  - guides/configuration.md/.ko.md — new "LLM-adapter properties"
                                     section documenting
                                     `ssrf.guard.springai.wrap-tool-callbacks`
                                     and the new
                                     `ssrf.guard.langchain4j.wrap-tool-executors`.
  - guides/security-model.md/.ko.md — the "what this doesn't protect"
                                       bullet was stale (mentioned
                                       RestClient only); now covers
                                       every module v3.1+ ships. Added
                                       a dedicated bullet about the
                                       WebClient URL-time → URL+DNS
                                       upgrade.

Autoconfig integration tests:
  - SsrfGuardLangchain4jAutoConfigurationTest — boots a Spring context
    with the autoconfig, declares a consumer @bean ToolExecutor, then
    asserts:
      * the BeanPostProcessor wrapped it (instanceof check)
      * end-to-end block on an AWS metadata URL (LLM sees the
        structured JSON error)
      * end-to-end allow on a whitelisted URL (delegate's
        PRETEND-FETCHED echo)
      * `wrap-tool-executors=false` correctly leaves the bean
        unwrapped — the off switch for manual-wrap workflows.

  - SsrfGuardWebClientAutoConfigurationTest extended to verify the
    new v3.1 beans (ssrfReactorClientHttpConnector,
    ssrfReactorAddressResolverGroup) are registered. Previously the
    test only checked filter / customizer / builder; the new beans
    silently going missing in a future refactor would have flown
    under the radar.

Tests now total 210 across 11 modules (up from 199 in PR #6 and
204 after PR #7).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant