type
design
tags
project/HotPlex
security/ssrf
security/url-validation
HotPlex v1.0 服务端请求伪造(Server-Side Request Forgery)防护设计。HotPlex 通过 AEP 协议代理 WebFetch 请求(Claude Code 的 WebFetch tool),可被滥用为 SSRF 跳板攻击内网服务或云元数据端点。
攻击者 ──→ HotPlex Gateway ──→ 内部服务
│
├── AWS EC2 元数据 (169.254.169.254)
├── Kubernetes API (10.0.0.1)
├── 数据库服务 (10.x.x.x)
└── 本地服务 (127.0.0.1:8888)
端点
IP/域名
风险
凭证
AWS EC2 Metadata
169.254.169.254
🔴 获取 IAM 角色凭证
EC2 Role
GCP Metadata
metadata.google.internal
🔴 获取服务账号令牌
GCP SA
Azure Metadata
169.254.169.254
🔴 获取 Managed Identity 令牌
Azure MI
Kubernetes API
10.0.0.1
🔴 集群控制面
K8s RBAC
Docker API
127.0.0.1:2375
🟡 容器逃逸/控制
无
内网数据库
10.x.x.x:5432
🟡 数据泄露
DB 凭证
场景
描述
影响
云元数据窃取
curl http://169.254.169.254/latest/meta-data/
获取 EC2 IAM 凭证
内网端口扫描
对 10.0.0.0/8 范围端口扫描
探测内网服务
本地端口探测
curl http://127.0.0.1:8888/admin
访问本地管理接口
云存储枚举
curl https://s3.amazonaws.com/...
S3 桶枚举(ACL 依赖)
Redis/DB 未授权访问
内网 Redis 无认证
数据读写
┌─────────────────────────────────────────────────────────┐
│ Layer 1: 协议层(最优先) │
│ ✅ 仅允许 http/https,禁止 file://, gopher:// 等 │
├─────────────────────────────────────────────────────────┤
│ Layer 2: IP 层 │
│ ✅ 阻断私有 IP范围 (10.x, 172.16.x, 192.168.x) │
│ ✅ 阻断链路本地 (169.254.x, 224.x.x.x) │
│ ✅ 阻断环回地址 (127.x.x.x, ::1, 0.0.0.0) │
├─────────────────────────────────────────────────────────┤
│ Layer 3: DNS 重绑定防护(防绕过) │
│ ✅ DNS 解析后再次验证 IP(解析结果可能被污染) │
├─────────────────────────────────────────────────────────┤
│ Layer 4: Host 头部验证 │
│ ✅ URL host 与最终解析 IP 一致性检查 │
└─────────────────────────────────────────────────────────┘
// internal/security/ssrf.go
import (
"net"
"net/url"
)
// BlockedCIDRs 禁止访问的 IP 范围
var BlockedCIDRs = []* net.IPNet {
// 私有地址
mustParseCIDR ("10.0.0.0/8" ), // 10.0.0.0 - 10.255.255.255
mustParseCIDR ("172.16.0.0/12" ), // 172.16.0.0 - 172.31.255.255
mustParseCIDR ("192.168.0.0/16" ), // 192.168.0.0 - 192.168.255.255
// 环回地址
mustParseCIDR ("127.0.0.0/8" ), // 127.0.0.0 - 127.255.255.255
mustParseCIDR ("::1/128" ), // IPv6 loopback
mustParseCIDR ("fc00::/7" ), // IPv6 unique local
mustParseCIDR ("fe80::/10" ), // IPv6 link-local
// 链路本地(云元数据)
mustParseCIDR ("169.254.0.0/16" ), // 169.254.0.0 - 169.254.255.255 (AWS/GCP/Azure)
// 广播地址
mustParseCIDR ("255.255.255.255/32" ),
// 保留地址
mustParseCIDR ("0.0.0.0/8" ), // 0.0.0.0 (Linux: 当前主机)
}
func mustParseCIDR (cidr string ) * net.IPNet {
_ , n , err := net .ParseCIDR (cidr )
if err != nil {
panic ("invalid CIDR: " + cidr )
}
return n
}
func isIPBlocked (ip net.IP ) bool {
for _ , blocked := range BlockedCIDRs {
if blocked .Contains (ip ) {
return true
}
}
return false
}
// internal/security/ssrf.go
type SSRFProtectionError struct {
URL string
Reason string
Blocked string // 被阻断的具体 IP/CIDR
}
func (e * SSRFProtectionError ) Error () string {
return fmt .Sprintf ("SSRF blocked: %s (reason=%s, blocked_by=%s)" , e .URL , e .Reason , e .Blocked )
}
// ValidateURL 验证 URL 是否安全(SSRF 防护)
// 验证顺序:协议 → 主机解析 → DNS 重绑定 → IP 阻断
func ValidateURL (targetURL string ) error {
// 1. 解析 URL
u , err := url .Parse (targetURL )
if err != nil {
return & SSRFProtectionError {URL : targetURL , Reason : "invalid URL" }
}
// 2. 协议检查:仅允许 http/https
switch u .Scheme {
case "http" , "https" :
// 允许
case "" :
return & SSRFProtectionError {URL : targetURL , Reason : "missing scheme" }
default :
return & SSRFProtectionError {URL : targetURL , Reason : "disallowed scheme: " + u .Scheme }
}
// 3. 主机名检查:拒绝裸 IP(防止 CIDR 绕过)
if ip := net .ParseIP (u .Hostname ()); ip != nil {
if isIPBlocked (ip ) {
return & SSRFProtectionError {
URL : targetURL ,
Reason : "bare IP in URL is blocked" ,
Blocked : ip .String (),
}
}
return nil // 直接 IP,无 DNS 重绑定风险
}
// 4. DNS 解析 + 重绑定防护
// 必须解析后再检查 IP(防止 DNS 指向内网)
ips , err := net .LookupIP (u .Hostname ())
if err != nil {
return & SSRFProtectionError {URL : targetURL , Reason : "DNS lookup failed: " + err .Error ()}
}
// 5. 检查所有解析结果
for _ , ip := range ips {
if isIPBlocked (ip ) {
return & SSRFProtectionError {
URL : targetURL ,
Reason : "DNS resolved to blocked IP" ,
Blocked : ip .String (),
}
}
}
// 6. 额外检查:如果是域名,验证不指向内网后再解析
// (可选)缓存合法域名减少 LookupIP 调用
return nil
}
⚠️ DNS 重绑定可绕过静态 IP 检查 :攻击者注册域名 evil.com 解析到公网 IP,但短时间内切换到内网 IP。
// ValidateURLWithRebindProtection DNS 重绑定防护
func ValidateURLWithRebindProtection (targetURL string , expectedTTL time.Duration ) error {
// 基础验证
if err := ValidateURL (targetURL ); err != nil {
return err
}
// 额外检查:DNS 解析后的 TTL 是否过短(疑似 DNS 重绑定)
// 注意:Go 标准库不提供 TTL 信息,需使用自定义 DNS 客户端
// 如需完全防护,集成 `miekg/dns` 进行 AXFR 查询
return nil
}
// 简化方案:强制二次解析(用于高敏感场景)
func ValidateURLDoubleResolve (targetURL string ) error {
// 第一次解析
if err := ValidateURL (targetURL ); err != nil {
return err
}
// 短暂延迟后再次解析(增加 DNS 缓存失效概率)
time .Sleep (100 * time .Millisecond )
u , _ := url .Parse (targetURL )
ips , _ := net .LookupIP (u .Hostname ())
for _ , ip := range ips {
if isIPBlocked (ip ) {
return & SSRFProtectionError {
URL : targetURL ,
Reason : "DNS rebind detected: IP changed to blocked range" ,
Blocked : ip .String (),
}
}
}
return nil
}
3.3 主机名校验(Open Redirect 防护)
// ValidateRedirectURL 验证跳转目标(防止 Open Redirect + SSRF 组合攻击)
func ValidateRedirectURL (redirectURL string ) error {
// 允许相对路径(安全)
if ! strings .HasPrefix (redirectURL , "http://" ) && ! strings .HasPrefix (redirectURL , "https://" ) {
return nil
}
// 绝对路径需要 SSRF 检查
return ValidateURL (redirectURL )
}
// internal/engine/aep_handler.go
// HandleWebFetch AEP WebFetch 事件处理
func (h * AEPHandler ) HandleWebFetch (event * Envelope ) (* Envelope , error ) {
url := event .Data ["url" ].(string )
// SSRF 防护验证
if err := ssrf .ValidateURL (url ); err != nil {
// 记录安全日志
log .Warn ("SSRF attempt blocked" ,
"url" , url ,
"user_id" , h .session .OwnerID ,
"session_id" , h .session .ID ,
"reason" , err .Error (),
)
return & Envelope {
Kind : "error" ,
Data : map [string ]interface {}{
"code" : "SSRF_BLOCKED" ,
"message" : "URL blocked by security policy" ,
"url" : url ,
},
}, nil
}
// 执行请求
return h .doWebFetch (url )
}
// SSRF 阻断日志字段
log .Warn ("SSRF blocked" ,
"session_id" , sessionID ,
"user_id" , userID ,
"url" , blockedURL ,
"resolved_ip" , resolvedIP ,
"blocked_cidr" , cidrBlock ,
"timestamp" , time .Now ().Unix (),
)
# configs/security.yaml
ssrf :
# 允许的协议
allowed_schemes :
- http
- https
# 阻断的 IP 范围(可追加)
blocked_ip_ranges :
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- 127.0.0.0/8
- 169.254.0.0/16
- ::1/128
- fc00::/7
- fe80::/10
# DNS 重绑定防护(高敏感场景)
dns_rebind_protection :
enabled : false # 启用会增性能开销
double_resolve : false
min_ttl_seconds : 60
# 域名白名单(完全信任的域名,跳过 SSRF 检查)
# ⚠️ 谨慎使用,确保这些域名不会被攻击者控制
allowlist :
- " api.github.com"
- " *.anthropic.com"
仅允许 http/https 协议
阻断私有 IP 段(10.x, 172.16.x, 192.168.x)
阻断链路本地地址(169.254.x)
阻断环回地址(127.x.x.x, ::1)
拒绝裸 IP URL(绕过 DNS 过滤)
DNS 解析后再次验证 IP(防重绑定)
SSRF 阻断日志(含 session_id, user_id)
域名白名单最小化(防止白名单域名被攻击者控制)
性能测试(DNS 解析不显著增加延迟)