// policy_test.go:SSRF 私网判定 / 重定向重验 / 域名 trie / robots 解析(A6.1 关键路径)。 // 全部测试不用外部网络(DNS 判定用注入 resolver 或字面量 IP)。 package policy import ( "context" "testing" ) // TestCheckURLSchemeProtocols 协议/端口白名单。 func TestCheckURLSchemePorts(t *testing.T) { g := &Guard{} cases := []struct { url string bad bool rule string }{ {"https://example.com", false, ""}, {"http://example.com", false, ""}, {"http://example.com:80/x", false, ""}, {"https://example.com:443/x", false, ""}, {"http://example.com:8080/x", true, "ssrf_port"}, {"file:///etc/passwd", true, "ssrf_scheme"}, {"ftp://example.com", true, "ssrf_scheme"}, {"gopher://example.com", true, "ssrf_scheme"}, {"http://localhost/x", true, "ssrf_private_ip"}, {"http://metadata.google.internal/x", true, "ssrf_metadata_host"}, {"http://foo.internal/x", true, "ssrf_metadata_host"}, {"http://kubernetes.default.svc/x", true, "ssrf_metadata_host"}, {"http://127.0.0.1/x", true, "ssrf_private_ip"}, {"http://10.1.2.3/x", true, "ssrf_private_ip"}, {"http://192.168.1.1/x", true, "ssrf_private_ip"}, {"http://172.16.0.9/x", true, "ssrf_private_ip"}, {"http://169.254.169.254/latest/meta-data/", true, "ssrf_private_ip"}, {"http://0.0.0.0/x", true, "ssrf_private_ip"}, {"http://[::1]/x", true, "ssrf_private_ip"}, {"http://[fe80::1]/x", true, "ssrf_private_ip"}, {"http://[fc00::1]/x", true, "ssrf_private_ip"}, } for _, c := range cases { d := g.CheckURL(c.url) if c.bad && d == nil { t.Errorf("%s 应被拒", c.url) } if !c.bad && d != nil { t.Errorf("%s 不应被拒: %v", c.url, d) } if c.bad && d != nil && c.rule != "" && d.RuleID != c.rule { t.Errorf("%s rule 期望 %s 得 %s", c.url, c.rule, d.RuleID) } } } // TestResolveCheckPrivateDNS 域名解析到私网 IP 必须拦。 // DNS 通路不依赖外部网络:字面量 IP 路径 + CheckHost 路径在本测试覆盖, // fake resolver 的 DNS 通路在集成测试覆盖(此处不留未用变量)。 func TestResolveCheckPrivateDNS(t *testing.T) { g := &Guard{} if d := g.CheckHost("192.168.1.61"); d == nil { t.Fatal("私网字面量应拒") } if d := g.CheckHost("example.com"); d != nil { t.Fatalf("普通域名不应拒: %v", d) } } // TestRedirectRecheck 每跳重验(含 URL 与 host 校验)。 func TestRedirectRecheck(t *testing.T) { g := &Guard{} if d := g.CheckRedirect(context.Background(), "http://10.0.0.5/next"); d == nil { t.Fatal("重定向到私网应拒") } if d := g.CheckRedirect(context.Background(), "file:///etc/passwd"); d == nil { t.Fatal("重定向到 file 应拒") } if d := g.CheckRedirect(context.Background(), "http://169.254.169.254/x"); d == nil { t.Fatal("重定向到 metadata 应拒") } } // TestTrieLookup 后缀匹配语义。 func TestTrieLookup(t *testing.T) { tr := NewDomainTrie() _ = tr.insertStub() if a, ok := tr.Lookup("api.example.com"); !ok || a != "direct" { t.Fatalf("example.com 应命中 direct: a=%s ok=%v", a, ok) } if a, ok := tr.Lookup("example.com"); !ok || a != "direct" { t.Fatalf("裸域应命中(插入形态不含前导点)") } if _, ok := tr.Lookup("notexample.com"); ok { t.Fatal("非后缀不应命中") } if a, ok := tr.Lookup("deep.sub.example.com"); !ok || a != "direct" { t.Fatalf("深层子域应命中") } // deny 优先:最长后缀 tr.Insert(".example.com", "direct") tr.Insert("deny.example.com", "deny") if a, _ := tr.Lookup("deny.example.com"); a != "deny" { t.Fatalf("更长后缀应胜出: %s", a) } if a, _ := tr.Lookup("other.example.com"); a != "direct" { t.Fatalf("未命中 deny 走 direct: %s", a) } if !tr.IsDenied("a.deny.example.com") { t.Fatal("IsDenied 应真") } } // TestTrieLoadFromStore 从 SQLite 加载。 func TestTrieLoadFromStore(t *testing.T) { db := openPolicyDB(t) defer db.Close() if err := db.RuleUpsert("suffix", ".internal.example", "deny", 1); err != nil { t.Fatal(err) } if err := db.RuleUpsert("suffix", "onesvm.com", "direct", 10); err != nil { t.Fatal(err) } tr := NewDomainTrie() n, err := tr.LoadFromStore(db) if err != nil || n != 2 { t.Fatalf("加载 2 条: n=%d err=%v", n, err) } gen1 := tr.Generation() // 热载 if err := db.RuleUpsert("suffix", ".blocked.cn", "deny", 2); err != nil { t.Fatal(err) } if _, err := tr.LoadFromStore(db); err != nil { t.Fatal(err) } if tr.Generation() != gen1+1 { t.Fatal("热载应递增代次") } if !tr.IsDenied("x.blocked.cn") { t.Fatal("热载后新规则应生效") } } // TestRobotsMatch robots 解析:UA 段 + 路径级判定(ITER-3 FIX-4:matchRobots 带 path, // 走 AllowedPath 最长匹配语义)。 func TestRobotsMatch(t *testing.T) { body := `User-agent: * Disallow: /private/ Allow: /private/ok/ Disallow: / ` // 「Disallow: /」全站禁(无更长匹配时)。 if matchRobots(body, "onesvm-browser-server", "/anything") { t.Fatal("命中 Disallow: /(最长匹配)应禁") } // 路径级:/private/ 禁、/private/ok/ 放行(Allow 更长胜)、/public 放行(更长规则不匹配该前缀)。 if AllowedPath(body, "/private/secret") { t.Fatal("/private/ 前缀应禁") } if !AllowedPath(body, "/private/ok/page") { t.Fatal("Allow 同长/更长应胜") } if AllowedPath(body, "/public") { t.Fatal("/public 命中 Disallow: / 应禁") } if !AllowedPath(body, "/private/ok/") { t.Fatal("/private/ok/ 精确命中 Allow 应放行") } } // TestRobotsAllowedPathLevel ITER-3 FIX-4 端到端:Allowed(缓存路径)按 path 判定—— // Disallow: /private/ 时 /private/data 拒、/public 放行。 func TestRobotsAllowedPathLevel(t *testing.T) { body := "User-agent: *\nDisallow: /private/\n" if matchRobots(body, "ua", "/private/data") { t.Fatal("/private/data 应拒") } if !matchRobots(body, "ua", "/public") { t.Fatal("/public 应放行") } } // TestRobotsAllowEmpty 空 robots 允许。 func TestRobotsAllowEmpty(t *testing.T) { if !matchRobots("", "ua", "/x") { t.Fatal("空 robots 应允许") } }