CVE-2026-22874 (GHSA-2r5c-gw76-rh3w) 的技术说明和参考资料:Gitea 的 Webhook 投递和仓库迁移中存在不完整的 SSRF 白名单,已在 Gitea 1.26.3 中修复。
我独立向 Gitea 维护者报告了这一 SSRF 类别,并在已发布的公告中被列为贡献者 (other);公告的主要报告者是 J. Leitschuh。本仓库是公开公告的防御性、教育性说明——该问题已修复。
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N)<= 1.26.2 — 修复版本:1.26.3Gitea 的默认 SSRF 白名单 (MatchBuiltinExternal,用于 Webhook 投递和仓库迁移) 依赖 Go 标准库 net.IP.IsPrivate(),该函数仅识别 RFC 1918 (10/8, 172.16/12, 192.168/16) 和 RFC 4193 (fc00::/7)。一些常用于云元数据服务、内部网络和 IPv6 过渡机制的地址范围在该定义下并非 "私有",因此通过了 "外部" 过滤器,使得已认证用户可以让服务器向这些目标发起 HTTP 请求。
由于 Webhook 投递会记录响应状态、头部以及最多约 1 MiB 的正文,并在 Webhook 历史界面中渲染,因此该 SSRF 并非盲打:攻击者可以读取目标内部服务返回的内容。
net.IP.IsPrivate() 遗漏的地址范围上述地址的 IPv4 映射 IPv6 形式同样受到影响。
因此,能够创建或修改 Webhook(或启动迁移)的已认证用户可以访问云元数据端点——例如通过 NAT64 映射访问 AWS IMDS,或位于 168.63.129.16 的 Azure WireServer——以及仅内部服务,然后通过 Webhook 历史视图读取响应。
默认策略是 Gitea 的 HostMatchList 检查中的 MatchBuiltinExternal,它将任何全局单播、非 IsPrivate() 的地址视为允许:
case MatchBuiltinExternal:
if ip.IsGlobalUnicast() && !ip.IsPrivate() {
return true
}
net.IP.IsPrivate() 并非 "外部" 的安全边界:它未考虑运营商级 NAT、云元数据端点或可路由到内部或元数据目标的 IPv6 过渡地址范围。
在 Gitea 1.26.3 中通过扩展内置过滤器以阻止上述额外地址范围来修复。操作员应升级到 1.26.3 或更高版本。作为纵深防御:
| 地址范围 | 含义 |
|---|
100.64.0.0/10 | RFC 6598 运营商级 NAT |
168.63.129.16/32 | Azure WireServer / 实例元数据端点 |
64:ff9b::/96 | RFC 6052 NAT64(可嵌入 169.254.169.254,例如 AWS IMDS) |
2001::/32 | RFC 4380 Teredo |
2002::/16 | RFC 3056 6to4 |