M48 复审的中危 finding:error.type 里的散文经 trace.UpstreamError 原样出镜,
因为守卫加在调用方(M46 的 model/voice 两处),构造函数自己不套守卫 ——
任何新调用点默认不安全,httpclient / vision / embedding / weixin 四条出口一直漏。
- trace.UpstreamError 构造时对 error.code / error.type 各走一次 ErrorToken
([A-Za-z0-9_.-]{<=64},不合规即空串):默认安全,不是可选加固。
- 构造后注入这条窄路收进 UpstreamErrorInfo.SetCode / SetType(同一道守卫),
供 DashScope 顶层 code 与 Anthropic 流内 error.type 两处使用 —— 调用方不再
各抄一份 ErrorToken。
- 删除因此冗余的调用方守卫:model.providerUpstreamError 与
voice.dashscopeOKBodyError。保留 voice 的 request_id 守卫(它不是 code/type,
构造函数看不见)。
- 四条漏的出口一行没改,只加用例证明守卫上移后自动安全;摘掉构造函数守卫时
四条用例 + model/voice 旧用例同时变红。
97 lines
4.3 KiB
Go
97 lines
4.3 KiB
Go
package trace
|
||
|
||
// 本文件是 M49 的核心断言:**守卫在构造函数里**。
|
||
//
|
||
// M46 把令牌守卫加在调用方(model / voice 各一份),M48 复审的中危 finding 证明那是
|
||
// 漂移形态 —— 构造函数自己不守,任何新调用点默认就不安全,四条出口(httpclient /
|
||
// vision / embedding / weixin)恰恰一直漏着。这里把断言钉在 UpstreamError 与
|
||
// SetCode / SetType 上:摘掉构造函数里的守卫,本文件与四条出口的用例同时变红。
|
||
//
|
||
// 每条都是「把它放宽就变红」的形状:断言的是真正进日志的两个出口(Error() / Attrs())。
|
||
|
||
import (
|
||
"bytes"
|
||
"errors"
|
||
"log/slog"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// TestUpstreamError_ConstructorDropsFreeTextCodeAndType 报文里 error.code 与
|
||
// error.type 都是回显的散文 ⇒ 两个字段在构造时就变空串,而 status / body_bytes /
|
||
// body_sha256 仍在(诊断面不能一起丢)。
|
||
func TestUpstreamError_ConstructorDropsFreeTextCodeAndType(t *testing.T) {
|
||
t.Parallel()
|
||
const prose = "Invalid value: '她刚才说的那句话'"
|
||
body := []byte(`{"error":{"code":"` + prose + `","type":"` + prose + `"}}`)
|
||
|
||
err := UpstreamError("api.example.com", 400, "application/json", body)
|
||
info, ok := err.(*UpstreamErrorInfo)
|
||
if !ok {
|
||
t.Fatalf("err 类型 = %T, want *UpstreamErrorInfo", err)
|
||
}
|
||
if info.Code != "" || info.Type != "" {
|
||
t.Fatalf("info.Code/Type = %q/%q,want 空串(构造即安全)", info.Code, info.Type)
|
||
}
|
||
|
||
var logBuf bytes.Buffer
|
||
slog.New(slog.NewJSONHandler(&logBuf, nil)).Error("upstream call failed", "err", err)
|
||
for _, surface := range []string{err.Error(), logBuf.String(), strings.Join(anyToStrings(info.Attrs()), " ")} {
|
||
if strings.Contains(surface, "她刚才说的那句话") {
|
||
t.Fatalf("散文经 code/type 进了出口:%s", surface)
|
||
}
|
||
if strings.Contains(surface, " code=") || strings.Contains(surface, " type=") {
|
||
t.Fatalf("非令牌形状的 code/type 没有被丢弃:%s", surface)
|
||
}
|
||
}
|
||
for _, want := range []string{"status=400", "body_bytes=", "body_sha256=", "api.example.com"} {
|
||
if !strings.Contains(err.Error(), want) {
|
||
t.Fatalf("error = %q,want 含 %q(诊断线索不能一起丢)", err.Error(), want)
|
||
}
|
||
}
|
||
if !errors.Is(err, ErrUpstream) {
|
||
t.Fatal("errors.Is(err, ErrUpstream) = false;降级链无法按类别判断")
|
||
}
|
||
}
|
||
|
||
// TestUpstreamError_ConstructorKeepsProtocolTokenCodeAndType 真实的协议枚举必须原样
|
||
// 通过:丢了它们只是少一条分类提示,但那是**有价值的**提示,别顺手一起丢。
|
||
func TestUpstreamError_ConstructorKeepsProtocolTokenCodeAndType(t *testing.T) {
|
||
t.Parallel()
|
||
body := []byte(`{"error":{"code":"invalid_request_error","type":"Throttling.RateQuota"}}`)
|
||
err := UpstreamError("api.example.com", 429, "application/json", body)
|
||
info := err.(*UpstreamErrorInfo)
|
||
if info.Code != "invalid_request_error" || info.Type != "Throttling.RateQuota" {
|
||
t.Fatalf("info.Code/Type = %q/%q,want 原值(令牌形状必须保留)", info.Code, info.Type)
|
||
}
|
||
for _, want := range []string{"code=invalid_request_error", "type=Throttling.RateQuota"} {
|
||
if !strings.Contains(err.Error(), want) {
|
||
t.Fatalf("error = %q,want 含 %q", err.Error(), want)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestUpstreamError_SetCodeGuardsPostConstructionInjection 构造后注入这条窄路也必须
|
||
// 是守卫点:SetCode / SetType 是唯一合法通道,散文进去 = 空串出来(覆盖语义)。
|
||
func TestUpstreamError_SetCodeGuardsPostConstructionInjection(t *testing.T) {
|
||
t.Parallel()
|
||
const prose = "Invalid value: '她刚才说的那句话'"
|
||
info := UpstreamError("dashscope.example.com", 400, "application/json", nil).(*UpstreamErrorInfo)
|
||
|
||
info.SetCode("Throttling.RateQuota")
|
||
info.SetType("rate-limit")
|
||
if info.Code != "Throttling.RateQuota" || info.Type != "rate-limit" {
|
||
t.Fatalf("info.Code/Type = %q/%q,want 令牌原值", info.Code, info.Type)
|
||
}
|
||
|
||
// 注入不受控的散文:两个字段都变空串(而不是保留上一次的值)。
|
||
info.SetCode(prose)
|
||
info.SetType(prose)
|
||
if info.Code != "" || info.Type != "" {
|
||
t.Fatalf("info.Code/Type = %q/%q,want 空串(SetXxx 必须过同一道守卫)", info.Code, info.Type)
|
||
}
|
||
if text := info.Error(); strings.Contains(text, "她刚才说的那句话") {
|
||
t.Fatalf("SetCode/SetType 让散文进了错误文本:%s", text)
|
||
}
|
||
}
|