Files
Cyrene_v2/internal/capability/browser/guard_test.go
T
AskaEth 2d211c2d16 feat(backthink,browser): 自排工具面 C1-3 + 浏览器两处偏离收口(X2+X3 同文件一并落)
X2 C1-3(自排工具面与提示词):
- request_think(thinktool.go,照 transcode/tool.go 范式)+ reason 必填/urgency 闭集 + 双守门
  (安全闸 + owner 判定:群聊/陌生人一律拒)
- **成对**进两张唤醒态允许集(quiet + awake;CAPACITY_TOOL_SETS 是整体替换)
- 提示词抽共用段落 + 自排前缀优先(不枚举工具清单);面板加 self_wake 标签与筛选轴
- 4 个变异自检(拿掉 owner 判定/只加一张表/放宽 urgency/前端标签恒等)

X3 收口(与 X2 共用 wire_services.go ⇒ 一并落):
- 删掉第三份 SSRF 实现(guard.go),改由 io.ValidateRequestURL 唯一判定 + 薄门面(保两个错误身份)
- 浏览器会话挂进 a.close(真机:真 Chrome 被关、无残留、生产未扰动)
- a.wireBrowser() 归位到 wire_runtime.go,删掉 ioToolOptions 的临时挂点
- 5 个变异自检

顺带补提交 C1-2 漏掉的 internal/config/runtimeconfig/backthink_test.go(ed50053 漏 add)。
2026-10-06 21:16:23 +08:00

109 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package browser
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
iotools "git.yeij.top/AskaEth/Cyrene_v2/plugins/tools/io"
)
// TestGuardURL_DeniesLoopbackAndPrivate 钉住守卫的**默认拒绝面**:回环、私网、
// 链路本地、非 http(s) 协议、云元数据端点一律拒绝 —— 这是 design/28 §4 第 1 条
// (服务端代取任意 URL 必须过闸)的可执行判据。
func TestGuardURL_DeniesLoopbackAndPrivate(t *testing.T) {
t.Parallel()
denied := []string{
"http://127.0.0.1/",
"http://localhost/",
"http://[::1]/",
"http://192.168.1.10/admin",
"http://10.0.0.5/",
"http://172.16.3.4/",
"http://169.254.169.254/latest/meta-data/",
"http://0.0.0.0/",
"file:///etc/passwd",
"ftp://example.com/",
"http:///no-host",
"",
}
for _, raw := range denied {
if _, err := GuardURL(context.Background(), raw, nil); err == nil {
t.Errorf("GuardURL(%q) = nil,应当拒绝", raw)
} else if !strings.Contains(err.Error(), "安全守卫拒绝") && !strings.Contains(err.Error(), "URL 被安全守卫拒绝") {
t.Errorf("GuardURL(%q) 的错误不像守卫拒绝: %v", raw, err)
}
}
}
// TestGuardURL_AllowsPublicAndAllowlisted 钉住**放行面**:公网地址放行;白名单里的
// 主机(同源自取:本地 searxng / 本机 upload 路由)即使落在回环也放行。
func TestGuardURL_AllowsPublicAndAllowlisted(t *testing.T) {
t.Parallel()
if _, err := GuardURL(context.Background(), "https://example.com/a?b=1", nil); err != nil {
t.Fatalf("公网地址被拒: %v", err)
}
if _, err := GuardURL(context.Background(), "http://93.184.216.34/", nil); err != nil {
t.Fatalf("公网字面 IP 被拒: %v", err)
}
// 白名单是**按主机名字符串**比较的(与 io/ssrf 的 isHostAllowed 同口径):
// 允许 `localhost` 不等于允许 `127.0.0.1` —— 两者是两个主机名,要各写一条。
exempt := []struct{ entry, raw string }{
{"127.0.0.1", "http://127.0.0.1:8080/x"},
{"http://localhost:8080", "http://localhost/x"},
{"LocalHost", "http://localhost:9999/x"},
}
for _, tc := range exempt {
if _, err := GuardURL(context.Background(), tc.raw, []string{tc.entry}); err != nil {
t.Errorf("白名单 %q 未放行 %q: %v", tc.entry, tc.raw, err)
}
}
}
// TestGuardURL_AllowlistEntryForms 钉住白名单条目的三种写法(裸主机 / host:port / 完整 URL)
// 都按主机名匹配 —— 少一种,面板里写的白名单就会静默失效。
func TestGuardURL_AllowlistEntryForms(t *testing.T) {
t.Parallel()
for _, entry := range []string{"127.0.0.1", "127.0.0.1:9", "http://127.0.0.1:9999/"} {
if _, err := GuardURL(context.Background(), "http://127.0.0.1:7777/", []string{entry}); err != nil {
t.Errorf("条目 %q 未命中: %v", entry, err)
}
}
}
// TestGuardURL_RejectsHostResolvingToPrivate 钉住 DNS 侧的判定:一个解析到私网的主机名
// 也要被拒(用 httptest 起在本机、把它的主机名当目标;`localhost` 已由字面量覆盖,
// 这里覆盖「解析后才发现是私网」这条路径)。
func TestGuardURL_RejectsHostResolvingToPrivate(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("ok"))
}))
defer srv.Close()
// httptest 的 URL 形如 http://127.0.0.1:<port>,字面 IP 路径已由上面的用例覆盖;
// 这里钉住「解析结果里含私网地址」的判定函数本身。
host := strings.TrimPrefix(srv.URL, "http://")
hostIP, _, _ := strings.Cut(host, ":")
if _, err := GuardURL(context.Background(), srv.URL, nil); err == nil {
t.Errorf("httptest 地址 %s 应被拒(字面回环)", hostIP)
}
}
// TestGuardURL_KeepsBothErrorIdentities 钉住收口(切片 X3)之后的**双错误身份**:
// 判定本体搬去了 `io.ValidateRequestURL`,但本包的契约没动 —— 组合根/工具层仍按
// `ErrBlockedURL` 分类降级(`ReasonBlockedURL` 与它同源),而排障时能顺着链回溯到**唯一**
// 判定源(`io.ErrSSRF`)。少任何一个身份,「收口只搬判定、不动契约」就只是句口号。
func TestGuardURL_KeepsBothErrorIdentities(t *testing.T) {
t.Parallel()
_, err := GuardURL(context.Background(), "http://169.254.169.254/latest/meta-data/", nil)
if !errors.Is(err, ErrBlockedURL) {
t.Errorf("拒绝丢了本包契约身份 ErrBlockedURL: %v", err)
}
if !errors.Is(err, iotools.ErrSSRF) {
t.Errorf("拒绝丢了判定源身份 iotools.ErrSSRF: %v", err)
}
}