Files
Cyrene_v2/internal/runtime/trace/errortoken.go
T
AskaEth fe3872ee28 fix(trace): 令牌守卫上移进 trace.UpstreamError,构造即安全
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 旧用例同时变红。
2026-10-26 19:13:38 +08:00

44 lines
2.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package trace
import "strings"
// 本文件是「上游错误信封里的自由文本字段能不能出镜」的唯一守卫。
//
// 背景:UpstreamErrorInfo 只解析 error.code / error.type 两个字段,而它们是**自由
// 文本面** —— 主流上游里它们确实是枚举(invalid_request_error、overloaded_error、
// Throttling.RateQuota …),但网关、代理与兼容层可以往里面塞任何东西,包括回显的
// 用户原文。因此这两个字段出镜前必须像令牌:短、只含标识符字符。
//
// 丢弃的代价只是少一条分类提示;保留一个不受控字段的代价是她的话进日志。这道守卫
// 原先在 runtime/model(safeErrorToken)与 runtime/multimodal/voice
// (dashscopeErrorToken)各有一份逐字同形的实现,M46 把同一份收敛到这里。
//
// 🔴 M49 收口:守卫的**应用点**同样收进本包 —— UpstreamError 构造时对 error.code /
// error.type 各调一次(构造即安全,调用点不必也不该再守),构造后的注入走
// UpstreamErrorInfo.SetCode / SetType(同一道守卫)。因此本包之外的 Go 代码里
// **不应出现 ErrorToken 调用**:出现了就意味着又回到「调用方逐点补」的漂移形态
// (M48 复审的 finding 正是这么漏的:httpclient / vision / embedding / weixin 四处)。
// maxErrorTokenBytes 是 error.code / error.type / request_id 允许出镜的长度上限。
// 64 字节足够容纳任何真实的枚举与 UUID 形状,又远小于「一段话」。
const maxErrorTokenBytes = 64
// ErrorToken 只放行「像协议令牌」的短串(字母、数字与 `_ . -`),其余一律丢弃并返回
// 空串 —— 包括带空格、标点、引号的散文,以及超长串。调用方据此决定「这个字段出不
// 出镜」,而不是先出镜再清洗。
func ErrorToken(s string) string {
s = strings.TrimSpace(s)
if s == "" || len(s) > maxErrorTokenBytes {
return ""
}
for _, r := range s {
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9':
case r == '_' || r == '.' || r == '-':
default:
return ""
}
}
return s
}