cplieger/ssrf

GitHub: cplieger/ssrf

Go语言编写的SSRF防护库,用于验证URL和IP地址以防止SSRF攻击。

Stars: 0 | Forks: 0

# SSRF [![CI](https://static.pigsec.cn/wp-content/uploads/repos/2026/06/ed94489ab0155756.svg)](https://github.com/cplieger/ssrf/actions/workflows/ci.yaml) [![Go Reference](https://pkg.go.dev/badge/github.com/cplieger/ssrf.svg)](https://pkg.go.dev/github.com/cplieger/ssrf) [![License: GPL-3.0](https://img.shields.io/badge/License-GPL--3.0-blue.svg)](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 验证, 安全传输, 开源项目, 日志审计, 环回地址过滤, 私有地址过滤, 网络安全, 链接本地地址过滤, 错误处理, 防御深度, 隐私保护