Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ The source of truth for the entries below is [docs/changelog.md](docs/changelog.

## [Unreleased]

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

### Added

- New module **`ssrf-guard-llm`** — framework-agnostic JSON tool-input validator (`ToolInputGuard` + `JsonToolInputGuard`). Holds the URL detection + policy validation logic the adapter modules share.
- New module **`ssrf-guard-langchain4j`** — wraps LangChain4j `ToolExecutor`. Same defense as `-springai` for the other major Java LLM framework. Spring auto-wrap via `BeanPostProcessor`; `SsrfGuardedToolExecutors.wrap(...)` for non-Spring.
- **GraalVM native-image hints** via `META-INF/spring/aot.factories` in `-llm`. Adapter modules get free AOT support from Spring Boot 3.

### Changed

- `ssrf-guard-springai` refactored to a ~30-line thin adapter over `-llm`. Public API unchanged.
- Error payload backed by typed `SsrfBlockPayload` record (was `Map.of`). Same JSON wire shape; better AOT introspection.

### Fixed

- **WebClient DNS-time defense gap closed.** `SsrfGuardReactorAddressResolverGroup` filters reactor-netty's resolved IPs against the private/loopback ranges — closes the v3.0.x DNS-rebinding window WebFlux apps had.

Full notes in [docs/changelog.md](docs/changelog.md#310--llm-core-extraction-langchain4j-webclient-dns-gap-graalvm-hints).

## [3.0.1] — Fix metrics bean classpath gate

### Fixed
Expand Down Expand Up @@ -58,7 +77,8 @@ semantic-release rollup. Tagged but **never published to Maven Central**.

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

[Unreleased]: https://github.com/devslab-kr/ssrf-guard/compare/v3.0.1...HEAD
[Unreleased]: https://github.com/devslab-kr/ssrf-guard/compare/v3.1.0...HEAD
[3.1.0]: https://github.com/devslab-kr/ssrf-guard/releases/tag/v3.1.0
[3.0.1]: https://github.com/devslab-kr/ssrf-guard/releases/tag/v3.0.1
[3.0.0]: https://github.com/devslab-kr/ssrf-guard/releases/tag/v3.0.0
[2.0.0]: https://github.com/devslab-kr/ssrf-guard/releases/tag/v2.0.0
Expand Down
6 changes: 3 additions & 3 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,17 @@ ToolCallback[] safe = SsrfGuardedToolCallbacks.wrap(raw, urlPolicy);
<dependency>
<groupId>kr.devslab</groupId>
<artifactId>ssrf-guard</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</dependency>
```

### Gradle (Kotlin DSL)

```kotlin
implementation("kr.devslab:ssrf-guard:3.0.0")
implementation("kr.devslab:ssrf-guard:3.1.0")
```

> **v2.0.0에서 업그레이드?** 메타 `kr.devslab:ssrf-guard:3.0.0`이 v2.0.0 API 유지 — `-core`, `-httpclient5`, `-restclient`를 transitive로 끌어옴. `kr.devslab.ssrfguard.security.*`를 직접 import한 코드는 업데이트 필요 — [v3.0.0 changelog](CHANGELOG.md#300--multi-module--llm-agent-ssrf-defense) 패키지 매핑 참고.
> **v2.0.0에서 업그레이드?** 메타 `kr.devslab:ssrf-guard:3.1.0`이 v2.0.0 API 유지 — `-core`, `-httpclient5`, `-restclient`를 transitive로 끌어옴. `kr.devslab.ssrfguard.security.*`를 직접 import한 코드는 업데이트 필요 — [v3.0.0 changelog](CHANGELOG.md#300--multi-module--llm-agent-ssrf-defense) 패키지 매핑 참고.

## 설정

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,17 @@ Auto-config picks it up — any `@Bean ToolCallback` gets wrapped via a `BeanPos
<dependency>
<groupId>kr.devslab</groupId>
<artifactId>ssrf-guard</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</dependency>
```

### Gradle (Kotlin DSL)

```kotlin
implementation("kr.devslab:ssrf-guard:3.0.0")
implementation("kr.devslab:ssrf-guard:3.1.0")
```

> **Upgrading from v2.0.0?** The meta `kr.devslab:ssrf-guard:3.0.0` keeps the v2.0.0 API working — pulls in `-core`, `-httpclient5`, `-restclient` transitively. Direct imports of `kr.devslab.ssrfguard.security.*` need updates — see the [v3.0.0 changelog](CHANGELOG.md#300--multi-module--llm-agent-ssrf-defense) for the package-rename mapping.
> **Upgrading from v2.0.0?** The meta `kr.devslab:ssrf-guard:3.1.0` keeps the v2.0.0 API working — pulls in `-core`, `-httpclient5`, `-restclient` transitively. Direct imports of `kr.devslab.ssrfguard.security.*` need updates — see the [v3.0.0 changelog](CHANGELOG.md#300--multi-module--llm-agent-ssrf-defense) for the package-rename mapping.

## Configuration

Expand Down
18 changes: 10 additions & 8 deletions docs/changelog.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,30 @@ ssrf-guard의 주요 변경 사항을 기록합니다.

## [Unreleased]

v3.1.0 작업 추적. 릴리즈된 항목은 태그 시 별도 섹션으로 이동.
## [3.1.0] — LLM 코어 추출, LangChain4j, WebClient DNS 공백 메움, GraalVM hints

### Added

- **`ssrf-guard-llm`** — 신규. 모든 LLM 툴 어댑터가 공유하는 JSON 트리 walk + URL 추출 + 정책 검증을 담는 framework-agnostic 코어 모듈. `ToolInputGuard` (인터페이스)와 `JsonToolInputGuard` (기본 구현) 노출. 이전에 `SsrfGuardedToolCallback` 내부에 있던 ~200줄 로직을 이쪽으로 이동.
- **`ssrf-guard-llm`** — 신규. 모든 LLM 툴 어댑터가 공유하는 JSON 트리 walk + URL 추출 + 정책 검증을 담는 framework-agnostic 코어 모듈. `ToolInputGuard` (인터페이스)와 `JsonToolInputGuard` (기본 구현) 노출. 이전에 `SsrfGuardedToolCallback` 내부에 있던 ~200줄 로직을 이쪽으로 통합.
- **`ssrf-guard-langchain4j`** — 신규. LangChain4j의 `ToolExecutor`를 wrap하는 어댑터 모듈. `ssrf-guard-springai`와 동일한 LLM 에이전트 SSRF 표면을 LangChain4j 생태계 (Java LLM 양대 프레임워크 중 다른 하나) 대응. Spring Boot 사용자는 `BeanPostProcessor`로 자동 wrap; 비-Spring/프로그래매틱 사용자용 `SsrfGuardedToolExecutors.wrap(...)` 헬퍼 제공.
- **GraalVM native-image 친화성.** `ssrf-guard-llm`이 `RuntimeHintsRegistrar`를 등록 (`META-INF/spring/aot.factories` 통해) — Spring Boot AOT 프로세서가 런타임 reflection surface (매 차단마다 Jackson이 직렬화하는 `SsrfBlockPayload` record, Jackson에 의해 `label`이 읽히는 `BlockReason` enum)를 인식. 어댑터 모듈 (`-springai`, `-langchain4j`, 모든 Spring 자동설정)은 Spring Boot 3 AOT가 무료로 처리 — `@Bean` 팩토리 메서드와 `BeanPostProcessor`만 호스팅하므로.

### Changed

- **`ssrf-guard-springai`가 thin adapter (~30줄)로 리팩토링.** 새 `-llm` 모듈의 `JsonToolInputGuard`에 위임. Public API 변경 없음 — `SsrfGuardedToolCallback`의 모든 생성자/메서드 시그니처 유지. v3.0.x 소비자는 API 변경 못 느낌 — 단지 `ssrf-guard-llm`을 transitive로 받게 됨.
- **에러 payload 형태 안정화.** SSRF 차단 시 LLM이 보는 JSON 객체가 이제 typed `SsrfBlockPayload` record (`{error, reason, url, message, guidance}`) 기반. v3.0.x와 wire 호환 — 동일 필드명, 동일 값. 이전 `Map.of(...)` 형태의 JDK-내부 `ImmutableCollections.MapN` 백킹 타입이 AOT introspect 안 되던 문제 해결. wire shape에 의존한 스크립트 변경 불필요; 기존 substring 테스트도 그대로 통과.

### Added (이어서)
### Fixed

- **GraalVM native-image 친화성.** `ssrf-guard-llm`이 `RuntimeHintsRegistrar`를 등록 (`META-INF/spring/aot.factories` 통해) — Spring Boot AOT 프로세서가 런타임 reflection surface (매 차단마다 Jackson이 직렬화하는 `SsrfBlockPayload` record, 그리고 `label`이 Jackson에 의해 읽히는 `BlockReason` enum)를 인식. 이전 `Map.of(...)` payload 형태가 JVM-내부 `ImmutableCollections.MapN` 타입을 써서 AOT가 introspect 못 했던 문제 해결. 어댑터 모듈 (`-springai`, `-langchain4j`, 모든 Spring 자동설정)은 Spring Boot 3 AOT가 무료로 처리 — `@Bean` 팩토리 메서드와 `BeanPostProcessor`만 호스팅하므로.
- **WebClient DNS-time 방어 공백 메움.** v3.0.x의 `ssrf-guard-webclient`는 URL-time 필터만 실행 — 화이트리스트를 통과한 호스트가 DNS 시점에 사설 IP로 resolve될 수 있었음 (전형적인 DNS 리바인딩 → 메타데이터 공격). 새 `SsrfGuardReactorAddressResolverGroup`이 reactor-netty의 `AddressResolverGroup`에 후킹해서 RestClient 모듈이 Apache HttpClient `DnsResolver` 단계에서 적용하는 것과 동일한 사설 IP 필터를 적용. WebFlux 앱도 이제 차단형 RestClient 앱이 갖던 2단계 방어 (URL + DNS) 동등하게 받음. reactor-netty 클래스패스 의존 — 비-Netty WebFlux 백엔드 (Jetty Reactive, Helidon)는 URL-time 필터만 작동하고 connector 교체는 스킵.

### Changed (이어서)
### Migration

- **에러 payload 형태 안정화.** SSRF 차단 시 LLM이 보는 JSON 객체가 이제 typed `SsrfBlockPayload` record (`{error, reason, url, message, guidance}`) 기반. v3.0.x와 wire 호환 — 동일 필드명, 동일 값. 기존 테스트가 substring 매치로 검증하고 그대로 통과; wire shape에 의존한 스크립트도 변경 불필요.
v3.1.0로 그냥 올리기 — 소비자 코드 변경 없음. 메타 `kr.devslab:ssrf-guard:3.1.0`도 v3.0.x처럼 `-core`, `-httpclient5`, `-restclient`를 transitive로 끌어옴. 새 모듈 (`-llm`, `-langchain4j`)은 opt-in — 좌표 추가 안 하면 따라오지 않음.

### Fixed
기존 `SecurityException` catch 코드는 그대로 동작 (`SsrfGuardException`이 여전히 `SecurityException` 서브클래스). 구조화 `e.reason()` 접근하려면 `SsrfGuardException`을 catch.

- **WebClient DNS-time 방어 공백 메움.** v3.0.x의 `ssrf-guard-webclient`는 URL-time 필터만 실행 — 화이트리스트를 통과한 호스트가 DNS 시점에 사설 IP로 resolve될 수 있었음 (전형적인 DNS 리바인딩 → 메타데이터 공격). 새 `SsrfGuardReactorAddressResolverGroup`이 reactor-netty의 `AddressResolverGroup`에 후킹해서 RestClient 모듈이 Apache HttpClient `DnsResolver` 단계에서 적용하는 것과 동일한 사설 IP 필터를 적용. WebFlux 앱도 이제 차단형 RestClient 앱이 갖던 2단계 방어 (URL + DNS) 동등하게 받음. reactor-netty 클래스패스 의존 — 비-Netty WebFlux 백엔드 (Jetty Reactive, Helidon)는 URL-time 필터만 작동하고 connector 교체는 스킵.
Native-image 소비자: `kr.devslab:ssrf-guard-llm:3.1.0` (또는 transitive로 끌어오는 모듈) 추가하면 AOT 프로세서가 등록된 hints 자동 인식.

## [3.0.1] — 메트릭 빈 classpath 게이트 수정

Expand Down
18 changes: 10 additions & 8 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,30 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and

## [Unreleased]

Tracking v3.1.0 work. Released items will move into their own section below when the tag lands.
## [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). Replaces ~200 lines of logic that previously lived inside `SsrfGuardedToolCallback`.
- **`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.

### Added (continued)
### Fixed

- **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 (also Jackson-touched for its `label`). Replaces the previous `Map.of(...)` payload form, which used JVM-private `ImmutableCollections.MapN` types that AOT couldn't introspect. 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.
- **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.

### Changed (continued)
### Migration

- **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. Existing tests assert on substring matches against those names and keep passing; if you scripted around the wire shape you don't need to change anything.
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.

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

- **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.
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

Expand Down
18 changes: 9 additions & 9 deletions docs/getting-started/installation.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@

## 모듈 선택

ssrf-guard v3.0.0은 HTTP 클라이언트 경계로 분리되어 있습니다. 실제로 쓰는 모듈만 고르면 됩니다.
ssrf-guard 3.0+은 HTTP 클라이언트 경계로 분리되어 있습니다. 실제로 쓰는 모듈만 고르면 됩니다.

=== "RestClient (Spring Boot 3.x 기본)"

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

Expand All @@ -29,7 +29,7 @@ ssrf-guard v3.0.0은 HTTP 클라이언트 경계로 분리되어 있습니다.
<dependency>
<groupId>kr.devslab</groupId>
<artifactId>ssrf-guard-resttemplate</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</dependency>
```

Expand All @@ -41,7 +41,7 @@ ssrf-guard v3.0.0은 HTTP 클라이언트 경계로 분리되어 있습니다.
<dependency>
<groupId>kr.devslab</groupId>
<artifactId>ssrf-guard-webclient</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</dependency>
```

Expand All @@ -53,7 +53,7 @@ ssrf-guard v3.0.0은 HTTP 클라이언트 경계로 분리되어 있습니다.
<dependency>
<groupId>kr.devslab</groupId>
<artifactId>ssrf-guard-feign</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</dependency>
```

Expand All @@ -65,7 +65,7 @@ ssrf-guard v3.0.0은 HTTP 클라이언트 경계로 분리되어 있습니다.
<dependency>
<groupId>kr.devslab</groupId>
<artifactId>ssrf-guard-springai</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</dependency>
```

Expand Down Expand Up @@ -108,7 +108,7 @@ ssrf-guard v3.0.0은 HTTP 클라이언트 경계로 분리되어 있습니다.
<dependency>
<groupId>kr.devslab</groupId>
<artifactId>ssrf-guard-jdkhttp</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</dependency>
```

Expand All @@ -123,7 +123,7 @@ ssrf-guard v3.0.0은 HTTP 클라이언트 경계로 분리되어 있습니다.
<dependency>
<groupId>kr.devslab</groupId>
<artifactId>ssrf-guard-okhttp</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</dependency>
```

Expand All @@ -135,7 +135,7 @@ ssrf-guard v3.0.0은 HTTP 클라이언트 경계로 분리되어 있습니다.
```

!!! tip "최신 버전"
`3.0.0`을 [Maven Central](https://central.sonatype.com/artifact/kr.devslab/ssrf-guard) 최신으로 교체.
`3.1.0`을 [Maven Central](https://central.sonatype.com/artifact/kr.devslab/ssrf-guard) 최신으로 교체.

!!! info "v2.0.0에서 올라온다면"
`kr.devslab:ssrf-guard` 좌표는 여전히 동작 — 메타 아티팩트가 `-core`, `-httpclient5`, `-restclient`를 끌어옴. 대부분 버전만 올리고 다시 빌드하면 끝. `kr.devslab.ssrfguard.security.*` 직접 import한 코드는 [v3.0.0 changelog](../changelog.md#300--multi-module--llm-agent-ssrf-defense)의 패키지 매핑 참고.
Expand Down
Loading