fix(vision): report only URL origin when an image source is rejected
fetchURL echoed the whole raw URL (path, query, userinfo) in the ErrUnsupportedSource message, so a rejected source carrying a signed query leaked it into logs and tickets. Route the URL exit through trace.HostOf and fall back to a placeholder instead of the raw string. Test pins origin-only output and goes red if the fix is reverted.
This commit is contained in:
1 parent
d47185feb8
commit
a5b3d40a74
2 files changed
+67
-1
No files matched your search
@@ -119,7 +119,7 @@ func decodeBase64(src string) (string, []byte, error) {
|
|||||||
// 网关/PROXY 返回的 HTML 错误页绝不会被当成 png 送进模型。
|
// 网关/PROXY 返回的 HTML 错误页绝不会被当成 png 送进模型。
|
||||||
func fetchURL(ctx context.Context, rawURL string, client *http.Client) (string, []byte, error) {
|
func fetchURL(ctx context.Context, rawURL string, client *http.Client) (string, []byte, error) {
|
||||||
if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") {
|
if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") {
|
||||||
return "", nil, fmt.Errorf("vision: %w: %q", ErrUnsupportedSource, rawURL)
|
return "", nil, fmt.Errorf("vision: %w: %q", ErrUnsupportedSource, sourceOrigin(rawURL))
|
||||||
}
|
}
|
||||||
u, err := url.Parse(rawURL)
|
u, err := url.Parse(rawURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -242,6 +242,19 @@ func fileExists(path string) bool {
|
|||||||
return err == nil && info.Mode().IsRegular()
|
return err == nil && info.Mode().IsRegular()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sourceOrigin 给出可进错误串的「来源」形态:只有 scheme://host(见
|
||||||
|
// trace.HostOf),路径、查询串与 userinfo 一律不出 —— 一个被拒的来源地址同样
|
||||||
|
// 可能带签名 query,原样回显等于把它写进日志与工单。
|
||||||
|
//
|
||||||
|
// 解析不出主机(`file:///…`、相对路径、畸形串)时回占位符而**绝不回落原串**:
|
||||||
|
// 回落就是泄漏,而空串会让错误文案失去「这次拒绝的是哪一类地址」的信息。
|
||||||
|
func sourceOrigin(rawURL string) string {
|
||||||
|
if origin := trace.HostOf(rawURL); origin != "" {
|
||||||
|
return origin
|
||||||
|
}
|
||||||
|
return "<unknown source>"
|
||||||
|
}
|
||||||
|
|
||||||
// defaultHTTPClient 构造 DevDocs/02-development-constraints.md §2.3
|
// defaultHTTPClient 构造 DevDocs/02-development-constraints.md §2.3
|
||||||
// 所述的 HTTP client。这里每次调用返回一个新 client,
|
// 所述的 HTTP client。这里每次调用返回一个新 client,
|
||||||
// 而不是包级全局变量——后者为约束所禁止。
|
// 而不是包级全局变量——后者为约束所禁止。
|
||||||
|
|||||||
@@ -78,3 +78,56 @@ func TestDescribeDropsURLFromBuildRequestError(t *testing.T) {
|
|||||||
t.Fatalf("err = %v, want ErrUpstream(分类不许被带偏)", err)
|
t.Fatalf("err = %v, want ErrUpstream(分类不许被带偏)", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestFetchURLUnsupportedSourceReportsOriginOnly 钉住 M55 的那处 URL 出口:
|
||||||
|
// fetchURL 拒绝非 http(s) 来源时,错误串里只许有**来源**(scheme://host,
|
||||||
|
// 见 trace.HostOf),不得带路径、查询串与 userinfo —— 被拒的地址同样可能带
|
||||||
|
// 签名 query,而这条错误会进日志与工单。
|
||||||
|
//
|
||||||
|
// 变异:把 `sourceOrigin(rawURL)` 改回 `rawURL` ⇒ 本用例必红(secret、路径、
|
||||||
|
// 用户名都会出现);把兜底改回空串 ⇒ 第三条用例失去 "<unknown source>"。
|
||||||
|
func TestFetchURLUnsupportedSourceReportsOriginOnly(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
wantOrigin string
|
||||||
|
leaks []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "带 query 的 ftp",
|
||||||
|
in: "ftp://files.example.com/a/b.png?access_token=" + urlCauseQuerySecret,
|
||||||
|
wantOrigin: "ftp://files.example.com",
|
||||||
|
leaks: []string{urlCauseQuerySecret, "access_token", "/a/b.png"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "带 userinfo 的 ftp",
|
||||||
|
in: "ftp://" + urlCauseUser + ":svc-pass@files.example.com/p.png",
|
||||||
|
wantOrigin: "ftp://[redacted]@files.example.com",
|
||||||
|
leaks: []string{urlCauseUser, "svc-pass", "/p.png"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "没有主机的 file",
|
||||||
|
in: "file:///etc/passwd?x=1",
|
||||||
|
wantOrigin: "<unknown source>",
|
||||||
|
leaks: []string{"/etc/passwd", "x=1"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
_, _, err := fetchURL(context.Background(), c.in, nil)
|
||||||
|
if !errors.Is(err, ErrUnsupportedSource) {
|
||||||
|
t.Fatalf("err = %v, want ErrUnsupportedSource", err)
|
||||||
|
}
|
||||||
|
msg := err.Error()
|
||||||
|
for _, leak := range c.leaks {
|
||||||
|
if strings.Contains(msg, leak) {
|
||||||
|
t.Fatalf("错误串带出了地址细节 %q:%v", leak, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.Contains(msg, c.wantOrigin) {
|
||||||
|
t.Fatalf("错误串没给出可读来源 %q:%v", c.wantOrigin, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user