cplieger/ssrf
GitHub: cplieger/ssrf
Go语言编写的SSRF防护库,用于验证URL和IP地址以防止SSRF攻击。
Stars: 0 | Forks: 0
# SSRF
[](https://github.com/cplieger/ssrf/actions/workflows/ci.yaml)
[](https://pkg.go.dev/github.com/cplieger/ssrf)
[](LICENSE)
Go 库,用于验证 URL 和 IP 地址以防止 SSRF 攻击。拒绝私有/回环/链路本地/CGNAT 地址,强制 HTTPS(可配置),检测 IPv6 转换机制绕过(6to4、NAT64、Teredo、IPv4 兼容),并提供具有 DNS 解绑保护的强化 HTTP 传输,通过一次解析语义和 `net.Dialer.Control` 钩子进行深度防御。仅标准库(基于属性的测试依赖 pgregory.net/rapid)。
## 安装
`go get github.com/cplieger/ssrf@latest`
## 使用
```
import "github.com/cplieger/ssrf"
// Validate a URL before fetching
if err := ssrf.ValidateURL("https://example.com/data.json"); err != nil {
log.Fatal(err)
}
// Use the hardened transport for all outbound requests
client := &http.Client{
Transport: ssrf.SafeTransport(),
CheckRedirect: ssrf.SafeRedirectPolicy(nil),
}
// Allow HTTP + HTTPS with custom ports
client = &http.Client{
Transport: ssrf.SafeTransport(
ssrf.WithAllowedSchemes("https", "http"),
ssrf.WithAllowedPorts(443, 80),
),
}
// Programmatic error handling
var ssrfErr *ssrf.Error
if errors.As(err, &ssrfErr) {
switch ssrfErr.Kind {
case ssrf.KindBadScheme:
// handle scheme error
case ssrf.KindNonPublicIP:
// handle blocked IP
case ssrf.KindBadPort:
// handle port restriction
}
}
// Check a pre-resolved IP directly
addr := netip.MustParseAddr("8.8.8.8")
if ssrf.IsPublicAddr(addr) {
// safe to connect
}
```
## API
### 类型
- `Option` — 用于配置 `SafeTransport` 的功能选项
- `Policy func(netip.Addr) bool` — 解析 IP 的允许/拒绝谓词
- `Resolver` — DNS 解析接口 (`LookupNetIP`)
- `Error` — 带有 `Kind`、`Host`、`Msg` 和 `Err` 字段的 SSRF 错误
- `ErrorKind` — 将 SSRF 验证失败分类的枚举类
### 函数
- `ValidateURL(raw string) error` — 检查方案是否为 HTTPS 且主机是公开的
- `IsPublicHost(host string) bool` — 返回主机/IP 是否是全球可路由的
- `IsPublicAddr(addr netip.Addr) bool` — 返回 IP 是否是全球可路由的
- `SafeRedirectPolicy(next) func` — 验证每个跳转的重定向策略
- `SafeRedirectPolicyWithSchemes(schemes, next) func` — 具有自定义方案集的重定向策略
- `SafeTransport(opts ...Option) *http.Transport` — 具有 DNS 解绑安全拨号 + Control 钩子的传输
- `AllowedSchemes(opts ...Option) map[string]struct{}` — 提取用于重定向策略的方案集
### 选项
- `WithPolicy(Policy) Option` — 注入自定义允许/拒绝 IP 谓词
- `WithDialer(*net.Dialer) Option` — 注入自定义 net.Dialer
- `WithResolver(Resolver) Option` — 注入自定义 DNS 解析器
- `WithAllowedPorts(...uint16) Option` — 限制出站端口(默认:仅 443)
- `WithAllowedSchemes(...string) Option` — 配置允许的 URL 方案(默认:仅 https)
- `WithLogger(*slog.Logger) Option` — 注入用于 SSRF 警告的结构化记录器(默认:slog.Default())
### 结构化错误
`ValidateURL` 和 `SafeTransport` 的拨号函数返回的所有错误都是 `*ssrf.Error`,带有 `Kind` 字段:
| Kind | 含义 |
|------|---------|
| `KindInvalidURL` | URL 无法解析 |
| `KindBadScheme` | 方案不在允许的集合中 |
| `KindEmptyHost` | 没有主机组件 |
| `KindLocalhost` | 指向 localhost |
| `KindBareHostname` | 无点的主机名 |
| `KindNonPublicIP` | IP 不是全球可路由的 |
| `KindDNSFailed` | DNS 解析失败 |
| `KindPolicyDenied` | 自定义策略拒绝了 IP |
| `KindBadPort` | 端口不在允许的集合中 |
### 深度防御:Dialer.Control 钩子
传输使用 **两层** IP 验证:
1. **一次解析** — DNS 只解析一次,所有 IP 验证,然后拨号器连接到字面 IP(防止 TOCTOU 通过 DNS 解绑)。
2. **`net.Dialer.Control` 钩子** — 在套接字创建时验证实际连接的 IP,在操作系统解析地址之后但在 TCP 握手之前。这反映了来自 [doyensec/safeurl](https://github.com/doyensec/safeurl)、[Stripe smokescreen](https://github.com/stripe/smokescreen)、[mccutchen/safedialer](https://github.com/mccutchen/safedialer) 的规范模式。
### 被阻止的 IP 范围
IPv4(RFC 6890 + RFC 5737 + RFC 2544):
- RFC 1918 私有、回环、链路本地、多播、未指定
- `0.0.0.0/8`(此主机)、`240.0.0.0/4`(保留/广播)
- `100.64.0.0/10`(CGNAT,RFC 6598)
- `192.0.0.0/24`(IETF 协议分配)
- `192.0.2.0/24`、`198.51.100.0/24`、`203.0.113.0/24`(TEST-NET 1/2/3)
- `198.18.0.0/15`(基准测试)
- `192.88.99.0/24`(已弃用 6to4 中继)
IPv6:
- 回环、ULA、链路本地、多播、未指定
- `100::/64`(丢弃-only,RFC 6666)
- `2001:2::/48`(基准测试,RFC 5180)
- `2001:db8::/32`(文档,RFC 3849)
- `3fff::/20`(文档,RFC 9637)
- `5f00::/16`(SRv6 SIDs,RFC 9602)
IPv6 转换机制(嵌入 IPv4 提取并重新验证):
- `2002::/16`(6to4,RFC 3056)
- `64:ff9b::/96`(NAT64 已知,RFC 6052)
- `64:ff9b:1::/48`(NAT64 本地,RFC 8215 — 完全阻止)
- `2001::/32`(Teredo,RFC 4380 — 客户端 IPv4 XOR-inverted 在位 96–127)
- `::/96`(已弃用 IPv4 兼容)
## 设计上不支持
以下功能是故意 **不实现** 的:
| 功能 | 理由 |
|---------|-----------|
| 自定义允许/拒绝 IP 列表 | `WithPolicy(func(netip.Addr) bool)` 已经提供了这个功能 |
| 主机名允许列表/拒绝列表 | 应用层策略,不是核心 SSRF 防御 |
| Happy Eyeballs(RFC 8305) | 安全库优先考虑正确性而不是速度 |
| 响应体大小限制 | 在应用层使用 `io.LimitReader` |
| 全面的 `2001::/23` 阻止 | 过于宽泛;一些子分配是全球可到达的。我们阻止特定的非路由子范围 |
| ISATAP 嵌入 IPv4 | 使用 `fe80::/64`(已阻止)或路由前缀,其中嵌入 IPv4 仅是信息性的 |
| DNS-over-HTTPS/TLS 解析器 | `WithResolver` 允许插入任何解析器实现 |
## 安全
有关漏洞报告,请参阅 [SECURITY.md](SECURITY.md)。
## 许可证
GPL-3.0 — 请参阅 [LICENSE](LICENSE)。
标签:6to4 安全, API 开发, CGNAT 安全, DNS Rebinding 防护, DNS解析, EVTX分析, Go 语言, GPL-3.0 许可, HTTPS 强制, HTTP 传输安全, IPv6 安全, IP 地址验证, NAT64 安全, SSRF 防护, Teredo 安全, URL 验证, 安全传输, 开源项目, 日志审计, 环回地址过滤, 私有地址过滤, 网络安全, 链接本地地址过滤, 错误处理, 防御深度, 隐私保护