Files
Cyrene_v2/internal/runtime/trace/errortoken.go
T
AskaEth 1b2576ecaf fix(weixin): 扫码上游字段过令牌守卫,errortoken 契约改写
复审 p2 两条:

① 第五个出口:get_qrcode_status 的 status 与 scaned_but_redirect 的
redirect_host 此前原样进 INFO;同一 status 经 setPhase 还从复用二维码那条
INFO(phase)二次出镜。三处一律过 trace.ErrorToken;**控制流仍用原值**
(状态机读 status.Status、路由读 status.RedirectHost、confirmingPhase 读
phase),用例同时钉住「日志变空串」与「路由/判定不变」两个方向。

② 顺带补扫出一处 URL 类:get_bot_qrcode 的 qrcode_img_content(上游给的
图片地址,可能是带签名的 URL 或 data: 内联图)原样进 INFO ⇒ 改走
trace.MaskQuery(凭据类 query 打码、保留路径、解析不出 host 回空串)。

③ errortoken.go 的契约改写:原「本包之外不应出现 ErrorToken 调用」已被
M50 的正当调用点证伪。新口径按**字段类别**判定:协议枚举字段(status/
code/type/request_id)可在渲染点调用;自由文本字段(message/description/
msg/wording)不得靠它出镜 —— 那类默认不带,要带必须 opt-in + RedactText。
dashscope 的 request_id 例外一并写入。

每条用例都做了变异验证(去掉守卫 / 把守卫挪进控制流 / 改回原串,均打红)。
2026-10-26 19:41:28 +08:00

60 lines
3.4 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(同一道守卫)。
//
// 🔴 M50 修正口径(**取代**原先「本包之外的 Go 代码里不应出现 ErrorToken 调用」的说法;
// 那条已被证伪:M50 之后本包外确有正当调用点)。判据是**字段的类别**,不是调用点的位置:
//
// - **协议枚举字段**(信封的 status / code / type、DashScope 的 request_id 这类
// 「一格一个值」的字段)**可以**在渲染点调用本守卫后再出镜。真实取值是短令牌 ⇒
// 原样通过;上游若在枚举位上塞散文 ⇒ 整体变空串。现行调用点:
// `plugins/channels/qq_obv11`(信封 status,两处)、`plugins/channels/weixin_ilink`
// (扫码 status 与 redirect_host)、`runtime/multimodal/voice/dashscope_error.go`
// (request_id —— M50 之前就存在的正当例外)。
// - **自由文本字段**(message / description / msg / wording / 服务端错误说明头 …)
// **不得**靠本守卫出镜。它们的问题不是形状(短的人话照样像令牌),而是取值的
// 控制权:上游想写什么就写什么,而它写的常常是被拒的请求正文 —— 也就是她的话。
// 这类字段的口径是**默认不带**;要带必须显式 opt-in,并在保留时过 RedactText
// (见 trace.UpstreamError 的 WithUpstreamMessage 与
// internal/channel/adapter/httpclient 的 WithEnvelopeMessage)。
//
// 简言之:本守卫回答「这个令牌形状的枚举值能不能露」,不回答「这段人话能不能露」。
// 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
}