devslab-kr/ssrf-guard
GitHub: devslab-kr/ssrf-guard
面向 JVM 平台的 SSRF 防护库,通过四层过滤机制为多种 HTTP 客户端及 LLM Agent 工具调用提供统一的出站请求安全校验。
Stars: 0 | Forks: 0
# ssrf-guard
**English** · [한국어](README.ko.md)
[](https://central.sonatype.com/artifact/kr.devslab/ssrf-guard)
[](https://github.com/devslab-kr/ssrf-guard/actions/workflows/ci.yml)
[](https://codecov.io/gh/devslab-kr/ssrf-guard)
[](LICENSE)
[](https://openjdk.org/projects/jdk/21/)
[](https://spring.io/projects/spring-boot)
📖 **[文档 → ssrf-guard.devslab.kr](https://ssrf-guard.devslab.kr/)**
## 可运行示例
这些是独立的 Spring Boot 项目,涵盖了下面记录的每个模块 —— 克隆,`./gradlew bootRun`,curl。无需复制粘贴;示例已经端到端配置好(包含冒烟测试)。
| 示例 | 展示内容 |
| --- | --- |
| [`ssrf-guard-demo`](https://github.com/devslab-kr/devslab-examples/tree/main/ssrf-guard-demo) | RestClient + RestTemplate + WebClient 全部通过同一个 `UrlPolicy` 配置。15 种模式攻击矩阵 endpoint,Micrometer 指标 |
| [`ssrf-guard-springai-demo`](https://github.com/devslab-kr/devslab-examples/tree/main/ssrf-guard-springai-demo) | ⭐ LLM agent SSRF 防御。模拟 LLM 驱动,无需 API key |
| [`ssrf-guard-feign-demo`](https://github.com/devslab-kr/devslab-examples/tree/main/ssrf-guard-feign-demo) | Spring Cloud OpenFeign `RequestInterceptor` 集成 |
| [`ssrf-guard-jdkhttp-demo`](https://github.com/devslab-kr/devslab-examples/tree/main/ssrf-guard-jdkhttp-demo) | `java.net.http.HttpClient` 封装 —— 库本身不依赖 Spring |
| [`ssrf-guard-okhttp-demo`](https://github.com/devslab-kr/devslab-examples/tree/main/ssrf-guard-okhttp-demo) | OkHttp `Interceptor` + `Dns` 集成 —— 同样不依赖 Spring |
完整索引请访问 [github.com/devslab-kr/devslab-examples](https://github.com/devslab-kr/devslab-examples)。
## 模块矩阵
选择与你的 HTTP client 匹配的模块。核心库 (`ssrf-guard-core`) 将作为传递依赖被引入。
| 模块 | 用例 | 需要 Spring? |
|---|---|---|
| **`ssrf-guard`** | Meta artifact — RestClient + HttpClient5 (兼容 v2.0.0) | ✅ |
| `ssrf-guard-restclient` | Spring 6.1+ `RestClient` | ✅ |
| `ssrf-guard-resttemplate` | Spring `RestTemplate` | ✅ |
| `ssrf-guard-webclient` | Spring WebFlux `WebClient` — URL 阶段过滤器 + reactor-netty DNS 阶段 IP 过滤器 (v3.1+) | ✅ |
| `ssrf-guard-feign` | Spring Cloud OpenFeign | ✅ |
| `ssrf-guard-llm` 🧩 | 与框架无关的 JSON tool-input 验证器 (v3.1+) — 被 LLM adapter 复用 | — |
| **`ssrf-guard-springai`** ⭐ | Spring AI `ToolCallback` URL 验证 — 基于 `-llm` 的轻量级 adapter | ✅ |
| **`ssrf-guard-langchain4j`** ⭐ | LangChain4j `ToolExecutor` URL 验证 — 为另一个 Java LLM 框架提供相同的防御 (v3.1+) | ✅ |
| `ssrf-guard-httpclient5` | 直接使用 Apache HttpClient 5 | — |
| `ssrf-guard-jdkhttp` | `java.net.http.HttpClient` | — |
| `ssrf-guard-okhttp` | OkHttp | — |
## 功能说明
你的服务发出的每一个外部 HTTP 调用,在真正打开 socket 之前,都会经过一个四层 SSRF 过滤器:
1. **URL 阶段检查 (前线)** — scheme / host / port / IP-literal-form / userinfo 会在成本最低的关卡被拒绝,时机早于任何 DNS lookup。可捕获混淆 IP 的绕过攻击 (例如 `http://2130706433/` → `127.0.0.1`)。
2. **DNS 阶段白名单二次检查** — 在解析 hostname 时,会再次应用相同的 host policy。
3. **私有网络 IP 过滤器** — loopback, RFC-1918, link-local (包含位于 `169.254.169.254` 的 AWS metadata), CGNAT, IPv6 ULA,**IPv4 映射的 IPv6 + 6to4 解嵌** (`::ffff:10.0.0.5` 和 `2002:0a00::` 会被正确归类为私有地址)。
4. **重定向重新验证** — 每一次 3xx 跳转都会经过相同的检查。攻击者无法通过将 `example.com` 加入白名单,然后重定向到 `169.254.169.254`。
解析器验证过的那个 `InetAddress` 数组,正是 HttpClient 传递给 `Socket.connect()` 的数组 —— TOCTOU 时间窗口已关闭。
## Spring AI tool calls — 全新的 SSRF 攻击面
将 URL 作为 tool 参数接收的 LLM agent 默认就是 SSRF 攻击向量:
```
@Tool("Fetch a URL")
String fetchUrl(String url) {
return restClient.get().uri(url).retrieve().body(String.class);
// ↑ attacker controls the URL — one-line SSRF
}
```
`ssrf-guard-springai` 会包装每一个 `ToolCallback`,使得 URL 形态的参数在 tool 运行前根据策略进行验证;如果拒绝,则会返回一个结构化的错误字符串,LLM 可以解析该字符串并从中恢复。
```
ToolCallback[] raw = ToolCallbacks.from(new MyTools());
ToolCallback[] safe = SsrfGuardedToolCallbacks.wrap(raw, urlPolicy);
```
自动配置会接管它 —— 任何 `@Bean ToolCallback` 都会通过 `BeanPostProcessor` 进行包装。
## 安装
### Maven
```
kr.devslab
ssrf-guard
3.1.1
```
### Gradle (Kotlin DSL)
```
implementation("kr.devslab:ssrf-guard:3.1.1")
```
## 配置
```
ssrf:
guard:
enabled: true # master switch
allowed-schemes: [ "http", "https" ]
allowed-ports: [ -1, 80, 443 ] # -1 = default port for the scheme
block-private-networks: true
reject-ip-literal-hosts: true # NEW v3.0.0 — block http://127.0.0.1, http://2130706433, etc.
reject-user-info: true # NEW v3.0.0 — block https://user:pass@host/...
follow-redirects: true
# Exact-match whitelist
exact-hosts:
- api.partner.com
- billing.example.org
# Suffix whitelist — `partner.com` covers `partner.com` AND any subdomain
# of it, but not `badpartner.com` (label-boundary match).
suffixes:
- partner.com
- example.org
connect-timeout: 5s
read-timeout: 10s
```
一旦该 starter 加入 classpath,Spring Boot 为你构建的每一个 `RestClient` 都会自动应用该策略 —— 消费者端无需额外配置。
## 用法
```
@Service
public class PartnerApi {
private final RestClient client;
public PartnerApi(RestClient.Builder builder) {
this.client = builder.build();
}
public Customer fetch(long id) {
// Whitelisted host → goes through. Anything not on the list throws
// SecurityException before the connection is opened.
return client.get()
.uri("https://api.partner.com/customers/{id}", id)
.retrieve()
.body(Customer.class);
}
}
```
当请求未被列入白名单时会发生什么:
```
kr.devslab.ssrfguard.core.SsrfGuardException: Host not allowed: evil.com
(reason=blocked_host, scheme=https, host=evil.com)
at kr.devslab.ssrfguard.core.UrlPolicy.reject(...)
```
`SsrfGuardException extends SecurityException` — v2.0.0 版本的 `catch (SecurityException e)` 代码依然有效。你可以捕获新的异常类型来读取 `e.reason()` (一个 `BlockReason` 枚举:`blocked_host`, `blocked_private_ip`, `blocked_ip_literal`, `blocked_userinfo`, `blocked_scheme`, `blocked_port`, `blocked_redirect`)。
## 可观测性 (通过 Micrometer 自动装配)
```
ssrf_guard_blocked_total{reason="blocked_private_ip", scheme="http"} 42
ssrf_guard_allowed_total{scheme="https"} 13042
```
此外,每次拦截都会产生一条结构化的 WARN 日志:
```
WARN k.d.s.core.UrlPolicy : ssrf-guard: Host not allowed: evil.com (reason=blocked_host, scheme=https, host=evil.com)
```
Tag 数量是有限的 (`reason` 是一个枚举,`scheme` 为 http/https) — Prometheus / Datadog / CloudWatch 可以轻松摄取。
## 性能
interceptor 在允许路径上的开销为 **每请求约 5 μs** (JMH, JDK 21) — 对于 100 ms 的远程 API 调用而言,这仅是 **0.005% 的开销**,在实际应用中几乎不可见。
| 热点路径 | 平均开销 | 备注 |
|---|---:|---|
| `UrlPolicy.validate` 允许通过 | ~5 μs | 占生产环境流量的 99%+ |
| `UrlPolicy.validate` 拦截 | 5-12 μs | 早期退出 (如 IP literal) 比晚期退出 (如白名单) 成本更低 |
| `JsonToolInputGuard` 小型 JSON | ~6 μs | 包含一个 URL 的 LLM tool 输入 |
| `JsonToolInputGuard` ~2 KB JSON | ~24 μs | 包含 3 个 URL 的 RAG 增强 tool 输入 |
完整的方法论、带标准差的各项具体数据及其解读方式:请参见 [`BENCHMARKS.md`](./BENCHMARKS.md)。你可以使用 `./gradlew :ssrf-guard-benchmarks:jmh` 自行复现。
## 自动配置注册了什么 (RestClient 模块)
当 `ssrf.guard.enabled=true` (默认值) 时,RestClient 自动配置会被激活并注册以下内容:
- `SafeDnsResolver` — 白名单 + 私有 IP 过滤器,接入到 Apache HttpClient 5 的连接管理器中
- `CloseableHttpClient` — 内置了该 resolver 并且 (启用重定向时) 包含 `SafeRedirectStrategy`
- `HttpComponentsClientHttpRequestFactory` — 配置了指定的连接/读取超时时间
- `UrlPolicy` — 前线 URL 阶段关卡 (scheme, host, port, IP-literal, userinfo)
- `SsrfGuardClientHttpRequestInterceptor` — 委托给该策略的 Spring `ClientHttpRequestInterceptor`
- `SsrfGuardMetrics` — 当存在 `MeterRegistry` 时由 Micrometer 提供支持,否则为空操作
- `RestClientCustomizer` (命名为 `ssrfRestClientCustomizer`) — 将工厂和 interceptor 绑定到 Spring Boot 自动构建的 `RestClient.Builder` 上
每个模块都有自己的自动配置 — `SsrfGuardRestTemplateAutoConfiguration`, `SsrfGuardWebClientAutoConfiguration`, `SsrfGuardFeignAutoConfiguration`, `SsrfGuardSpringAiAutoConfiguration`。它们全都复用相同的 `UrlPolicy` 和 `SsrfGuardMetrics` bean。每个 bean 都是 `@ConditionalOnMissingBean`,因此你可以替换其中任何一个部分。
## 环境要求
- Java 21+
- Spring Boot 3.5+ (适用于基于 Spring 的模块)
- Spring AI 1.0+ (适用于 `springai` 模块)
- Spring Cloud 2024.0+ (适用于 `feign` 模块)
- Apache HttpClient 5 (由 `-httpclient5`, `-restclient`, `-resttemplate` 传递依赖引入)
## 许可证
Apache License 2.0 — 详见 [LICENSE](LICENSE) 和 [NOTICE](NOTICE)。
由 [Devslab](https://devslab.kr) 构建 · [DevsLab 开源工具包](https://github.com/devslab-kr) 的一部分。
标签:CISA项目, Spring Boot, SSRF防护, 域名枚举, 网络安全, 隐私保护