- 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 落了第三份同策实现待收口;浏览器会话未挂进程关闭链
91 lines
3.6 KiB
Go
91 lines
3.6 KiB
Go
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)
|
||
}
|
||
}
|