Files
Cyrene_v2/internal/capability/browser/guard_test.go
T
AskaEth f1fbc249fd
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(browser): B1 接线 + B2 薄层工具面(V5)
- browsercfg 接进组合根:配置读 $DATA_DIR/config/plugins/browser.json(零 env)→ 探测(本机
  Chromium → 已在跑的 CDP)→ 不可用就不注册(原因码入日志)
- 自研最小出站 CDP 客户端(不 Enabled Runtime、隔离世界求值、不发 --enable-automation)+
  导航 + 截图 0600 落盘 + 指纹自检;工具 browser(open/screenshot/info,owner-only)
- 3 个变异自检(启动恢复 --enable-automation/无视可用性/空闲回收失效)
- 真机对照(两站各 1 次,**如实结论**):两侧都 pass ⇒ 「headless 被拦 / headful 通过」**未被证明**;
  但测到两处硬差异:无头 UA 带 HeadlessChrome、screen 800 vs 2632;两模式 webdriver 均 false
  (把 L6 的推断变成实测)。跑完 0 残留 Chrome、无对外监听端口
⚠️ 未做:MCP 自动接入(口径委派未落地)/自动注入上下文/面板页/搜索降级/docker 配方/Windows 用
  pipe(Go ExtraFiles 不支持);SSRF 落了第三份同策实现待收口;浏览器会话未挂进程关闭链
2026-10-06 20:25:16 +08:00

91 lines
3.6 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"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// 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)
}
}