Skip to content

[code-review] skill #import:<url> SSRF 守卫不完整:重定向目标不做 DNS 私网复检 + 首查 fail-open,注释承诺的 rebinding 防护未兑现 #2793

Description

@topcheer

文件和行号
internal/tool/skill_portable.go:240-291(openSkillSource)

问题描述
注释(251-253 行)声称三层 SSRF 守卫:字面私网主机封堵、DNS 解析私网封堵(rebinding)、重定向复检。但实现存在两个缺口:

  1. CheckRedirect(272-280 行)只对重定向目标 hostname 做字面 isPrivateHost 检查(web_fetch.go:263,仅匹配 localhost/.internal 等字面名与 ParseIP 可解的 IP 字符串),不对重定向目标重新做 DNS 解析+私网 IP 检查。
  2. 首次请求的 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 模式下可全自动触发)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions