fix(images): 生图 url 回填下载拒绝私网目的地并只信字节嗅探

账户开关 images_url_to_b64_json 下载的图片 url 来自上游响应,是不可信输入。此前沿用
base_url 的 security.url_allowlist 策略,默认配置下只做格式校验,上游返回
localhost:8080 之类地址时网关会真的去请求内网。

- 下载 URL 主机为 localhost、回环、私网、链路本地、未指定地址的字面量时直接拒绝,
  不受 security.url_allowlist 配置影响(urlvalidator 导出 IsBlockedHost)
- 新增 ctx 标记 WithHTTPUpstreamPublicHostsOnly:共享上游客户端对带标记的请求无视配置
  校验解析后 IP,并为其克隆客户端挂 redirectChecker,重定向保持跟随但每一跳都校验
- 下载内容改为只信 http.DetectContentType 嗅探结果且限 png/jpeg/webp/gif,
  响应头不再作为依据
- 下载失败仍只记日志并保留原样;超时与端口策略不变
This commit is contained in:
feeeei
2026-09-05 15:50:56 +08:00
parent 3c53ba01a7
commit c227863d51
8 changed files with 243 additions and 20 deletions
@@ -162,6 +162,12 @@ func isAllowedHost(host string, allowlist []string) bool {
return false
}
// IsBlockedHost 报告 host 是否为 localhost、*.localhost,或回环、私网、链路本地、未指定地址的字面量 IP。
// 只判断字面量,域名的解析结果由 ValidateResolvedIP 校验。
func IsBlockedHost(host string) bool {
return isBlockedHost(strings.ToLower(strings.TrimSpace(host)))
}
func isBlockedHost(host string) bool {
if host == "localhost" || strings.HasSuffix(host, ".localhost") {
return true
@@ -73,3 +73,20 @@ func TestValidateHTTPURL(t *testing.T) {
t.Fatalf("expected localhost to be blocked when allow_private_hosts is false")
}
}
func TestIsBlockedHost(t *testing.T) {
for _, host := range []string{
"localhost", "LOCALHOST", "foo.localhost", " 127.0.0.1 ",
"127.0.0.1", "::1", "10.0.0.5", "172.16.0.1", "192.168.1.1",
"169.254.169.254", "0.0.0.0", "::", "fe80::1", "fc00::1", "::ffff:127.0.0.1",
} {
if !IsBlockedHost(host) {
t.Fatalf("expected %q to be blocked", host)
}
}
for _, host := range []string{"example.com", "cdn.example.com", "93.184.216.34", "8.8.8.8", "2606:4700:4700::1111", ""} {
if IsBlockedHost(host) {
t.Fatalf("expected %q to be allowed", host)
}
}
}