Skip to content

v3.1 (1/N): extract ssrf-guard-llm + add ssrf-guard-langchain4j#6

Merged
jlc488 merged 1 commit into
mainfrom
v3.1.0/llm-extraction
May 23, 2026
Merged

v3.1 (1/N): extract ssrf-guard-llm + add ssrf-guard-langchain4j#6
jlc488 merged 1 commit into
mainfrom
v3.1.0/llm-extraction

Conversation

@jlc488

@jlc488 jlc488 commented May 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

First of several v3.1 PRs. Extracts the LLM-agent SSRF defense's reusable core into a new framework-agnostic module and adds a second adapter for LangChain4j.

What changed

New: ssrf-guard-llm

Framework-agnostic core. Holds the JSON walking + URL extraction + policy validation that every LLM tool adapter shares. Exposes:

  • ToolInputGuard (interface) — the neutral contract: String checkOrFormatError(String toolInput)
  • JsonToolInputGuard (default impl) — Jackson-based JSON walker, validates every http(s):// string it finds at any depth against UrlPolicy. Optionally throws on violation (CI mode) or returns a structured JSON error string (LLM-recovery mode).

New: ssrf-guard-langchain4j

Adapter wrapping LangChain4j's ToolExecutor. SsrfGuardedToolExecutor inspects ToolExecutionRequest.arguments() through JsonToolInputGuard.

  • Auto-wrap for Spring Boot users via BeanPostProcessor (mirrors ssrf-guard-springai's pattern).
  • Manual SsrfGuardedToolExecutors.wrap(map) / wrap(collection) / wrapOne(executor) helpers for non-Spring / programmatic use (the Map<ToolSpecification, ToolExecutor> shape AiServices expects).

Targets LangChain4j 1.15.0 (current GA line).

Refactored: ssrf-guard-springai

SsrfGuardedToolCallback shrunk to ~30 lines — delegates to JsonToolInputGuard from -llm. Public API unchanged — same constructors, same methods, same behaviour. v3.0.x consumers see no API change; they just pick up ssrf-guard-llm transitively.

Why this shape

User asked the right question: "If two adapters have the same implementation and only the framework interface differs, isn't a common core + thin adapters the better design than copying ~200 lines twice?"

Yes. With the extraction:

Before After
Where URL detection lives Inside SsrfGuardedToolCallback (~200 lines) JsonToolInputGuard in -llm (single source)
Adapter size ~250 lines each ~30 lines each
New framework cost Copy 250 lines + maintain in parallel 30 lines + auto-config
User experience Zero code, just config Zero code, just config (unchanged)

The new ToolInputGuard interface also gives advanced users (custom dispatchers, MCP servers, internal tool routers) a clean hook — they DI JsonToolInputGuard and call checkOrFormatError(input) themselves. Optional, not required.

Non-breaking for v3.0.x consumers

ssrf-guard-springai's public types (SsrfGuardedToolCallback, SsrfGuardedToolCallbacks, SsrfGuardSpringAiAutoConfiguration) keep the same signatures. The only observable difference: an extra transitive dep on ssrf-guard-llm (which pulls Jackson, same as before).

Test plan

  • All 199 tests across 11 modules pass (was 182 + 17 new) — local full build green
  • :ssrf-guard-llm:test — 9 tests covering JSON walk, nested URLs, arrays, non-JSON input, throw-on-violation
  • :ssrf-guard-springai:test — 10 existing tests still pass after refactor
  • :ssrf-guard-langchain4j:test — 7 tests mirroring springai's threat model on the LangChain4j abstraction
  • CI green on this PR

What's NOT in this PR (follow-ups)

  • WebClient reactor-netty DNS-time guard (Phase 4)
  • GraalVM Native Image hints (Phase 5)
  • Tag v3.1.0 + release + demo bumps (Phase 6)

Splits the LLM-agent SSRF defense across two concerns:

1. ssrf-guard-llm (new module) — framework-agnostic core that holds the
   JSON walking + URL extraction + policy validation logic. Exposes
   ToolInputGuard (interface) and JsonToolInputGuard (default impl).

2. Adapter modules become ~30-line thin wrappers that translate between
   each LLM framework's own ToolExecutor/ToolCallback interface and the
   neutral ToolInputGuard contract.

Refactored:
  - ssrf-guard-springai: SsrfGuardedToolCallback delegates to
    JsonToolInputGuard via the new -llm api. Public API unchanged
    (constructors and methods keep the same shape) — v3.0.x consumers
    pick up -llm transitively and see no behavioural change. All 10
    existing springai tests pass.

Added:
  - ssrf-guard-langchain4j: SsrfGuardedToolExecutor wraps a LangChain4j
    ToolExecutor, inspects ToolExecutionRequest.arguments() through
    the same JsonToolInputGuard. Auto-wrap via BeanPostProcessor for
    Spring Boot consumers; SsrfGuardedToolExecutors.wrap(...) helpers
    for non-Spring / programmatic use. Targets LangChain4j 1.15.0 (the
    first GA line).

Why split this way

Previously the JSON walk + URL detection + error formatting lived
inside SsrfGuardedToolCallback (~200 lines). Adding LangChain4j the
naive way meant copying those 200 lines verbatim into
SsrfGuardedToolExecutor and fixing bugs in two places forever.

With the extraction, a fix to URL detection or error formatting lands
in one place (ssrf-guard-llm). New frameworks (MCP, custom tool
dispatchers, future Mistral/Cohere SDKs) get ~30 lines of adapter
work, not 200.

User experience is unchanged:

  - Spring AI users: install ssrf-guard-springai, declare regular
    @bean ToolCallback, autoconfig BeanPostProcessor wraps it.
  - LangChain4j users: install ssrf-guard-langchain4j, declare regular
    @bean ToolExecutor, autoconfig BeanPostProcessor wraps it.
  - No user code touches ToolInputGuard unless they want to roll their
    own dispatcher.

Build state

  All 199 tests across 11 modules pass (was 182 + 17 new). Local full
  build green. Maven publish coordinates declared but not yet released
  — that's Phase 6 (CHANGELOG + tag).

  Remaining v3.1 phases (separate PRs):
    - Phase 4: WebClient reactor-netty DNS-time guard
    - Phase 5: GraalVM Native Image hints
    - Phase 6: CHANGELOG + tag v3.1.0
@jlc488
jlc488 merged commit a19702b 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