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)。
109 lines
4.5 KiB
Go
109 lines
4.5 KiB
Go
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)
|
||
}
|
||
}
|