package browser import ( "context" "fmt" "net/url" iotools "git.yeij.top/AskaEth/Cyrene_v2/plugins/tools/io" ) // GuardURL 是浏览器侧访问 SSRF 判定的**唯一入口**,判定本体**只有一份**: // `plugins/tools/io.ValidateRequestURL`(design/28 §4:「复用既有 `io/ssrf` 白名单, // **不新增第三份实现**」)。 // // **为什么这里只留一层薄门面(切片 X3 的收口)**:本包原先自带一份与 io 同策的实现 // (`guard.go`:自己的 scheme 判定 + 网段表 + 白名单比对),只因为当时 io 侧的 // `validateRequestURL` 没导出、该包又不在那一片的文件面里。两份同策代码的危险不在 // 当下的结论,而在**以后只改一处** —— 改完两边静默分叉,浏览器侧的导航就成了绕过面。 // 收口后本文件只做两件事: // // 1. 调 `io.ValidateRequestURL`(**唯一**判定源:scheme → 白名单短路 → 字面 IP 网段 // 否决 → 解析后再按同一张表否决); // 2. 把它的错误翻成本包对外的 `ErrBlockedURL` —— 这是**契约**不是判定: // `ErrBlockedURL` / `ReasonBlockedURL` 是工具层与组合根(含后续 M3 面板入口)用来 // 分类降级与记日志的标识,收口只搬判定,不动契约。 // // 两个错误身份都保住(`%w:%w`):`errors.Is(err, ErrBlockedURL)` 成立(本包契约), // 网段/解析类拒绝上 `errors.Is(err, iotools.ErrSSRF)` 同样成立(判定源),io 侧的原因 // 文案(哪个网段、解析到哪个地址)一字不丢地进日志。形状错误(空 URL、非 http(s)) // 在 io 侧本就不带 ErrSSRF,那几档只有本包身份。 // // 为什么浏览器侧必须有这道闸:导航是**服务端代取用户/模型给的任意 URL**,不过闸就 // 等于把云元数据端点(`169.254.169.254`)、内网管理面暴露给一次提示词注入 // (design/28 §4 第 1 条;M3 面板入口同样过闸)。 func GuardURL(ctx context.Context, rawURL string, allowed []string) (*url.URL, error) { u, err := iotools.ValidateRequestURL(ctx, rawURL, allowed) if err != nil { return nil, fmt.Errorf("%w:%w", ErrBlockedURL, err) } return u, nil }