v3.1 (2/N): WebClient DNS-time defense via reactor-netty AddressResolverGroup#7
Merged
Merged
Conversation
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).
3 tasks
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the WebClient module's DNS-time defense gap. Before this PR,
ssrf-guard-webclientonly 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'sDnsResolverlayer, but for reactor-netty'sAddressResolverGroup.What's added
SsrfGuardReactorAddressResolverGroupWraps Netty's
DefaultAddressResolverGroup. Filters resolvedInetSocketAddressinstances using the sameNetUtil.isPrivateOrLocalpredicate the other modules use.Two paths covered:
doResolve(single result) — fails withUnknownHostExceptionif 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:
UnknownHostExceptionAutoconfig changes
SsrfGuardWebClientAutoConfigurationnow publishes:SsrfGuardReactorAddressResolverGroupbeanReactorClientHttpConnectorbean built onHttpClient.create().resolver(resolverGroup)The existing
ssrfWebClientCustomizerinjects the connector viaObjectProvider<ReactorClientHttpConnector>and calls.clientConnector(connector)on the builder — additive to the existing.filter(...)call.Both wired with
@ConditionalOnMissingBean:ReactorClientHttpConnector(custom pool, proxy, mTLS) winReactorNettyConnectorConfigurationinner class is gated by@ConditionalOnClass(HttpClient.class))Either way, the URL-time filter keeps working.
Test plan
Non-breaking
SsrfGuardExchangeFilterFunctionandSsrfGuardedToolCallbacksignatures unchanged. The new connector bean only activates when reactor-netty is present and no otherReactorClientHttpConnectorbean is in the context. Consumers who never call.resolver(...)themselves still benefit immediately on upgrade.What's NOT in this PR (next phase)