Files
Cyrene_v2/plugins/channels/telegram/botapi_leak_test.go
T
AskaEth 0f9251b983 fix(channel): 渠道适配器与出站工具的上游响应体/带凭据 URL 不再进日志与错误(M41)
接 M38 的 18 条真泄漏里「渠道适配器 + 出站工具」这一族(#10 #11 #12 #13 #15 #16)。
只动日志与 error 文字:发送/接收、解析口径、重试次数一个都没改。

- httpclient.do:非 2xx 不再把 512B 响应体原文拼进 error,改 trace.UpstreamError
  (状态/体积/sha256 前缀)+ 信封 code/message(RedactText + 200 rune);error 里的
  target 改 trace.HostOf,并剥掉 *url.Error 外壳(它带整条地址)
- telegram doJSON:4xx 只解 error_code/description(清洗 + 截断),体其余字段不拼;
  传输错误只留来源 —— Bot API 把 token 写在地址路径里
- weixin_ilink doJSON:4096B 体 ⇒ trace.UpstreamError,rawURL ⇒ mediafetch.HostOf;
  uploadOnce 剥掉预签名上传地址(encrypted_query_param),重试日志补来源主机
- qq_obv11 connectClient 成功 INFO:改走同包已有的 maskAccessTokenQuery
  (OneBot 惯例把 ?access_token= 写进 remote_url)
- plugins/tools/io http 拦截日志:改记 trace.HostOf + 原因码,不记原 URL、也不记错误
  原文(解析失败时它包着 *url.Error)

每条都有能变异打红的用例:QQ/iLink 预签名地址的哨兵、三处 4xx 体哨兵、
user:pass@ 与 query 凭据;八处变异逐条验证变红。
2026-10-26 13:47:56 +08:00

104 lines
4.3 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 telegram
// 本文件钉住 Bot API 出站调用的两条出镜边界(M41 #12/#13 那一族):
//
// - **非 200 的响应体原文不进错误**:旧实现把 512B 体原文当 description 拼进
// error,而 `ReadMessages` 会以 `long poll failed` 落日志 —— 上游在体里回显什么,
// 日志里就有什么。现在只解 error_code 与 description(清洗、截断后)。
// - **地址里的 bot token 不进错误**:Bot API 的地址是 /bot<token>/<method>,
// 传输层失败时 net/http 的 *url.Error 会把整条地址拼进错误串。
//
// 两条都是「改回去就变红」的形状:断言的是错误文本 —— 也就是真正被写进日志的字符串。
import (
"context"
"errors"
"net/http"
"strings"
"testing"
)
// failingTransport 让 a.client.Do 以传输层错误收场(只有它会让 net/http 包出带完整
// 地址的 *url.Error,那正是要钉住的泄漏形态)。
type failingTransport struct{}
func (failingTransport) RoundTrip(*http.Request) (*http.Response, error) {
return nil, errors.New("dial tcp: connection refused")
}
// TestDoJSON_StatusErrorDoesNotEchoBodySentinel 4xx 体里放哨兵 ⇒ 错误里搜不到。
//
// 哨兵刻意放在 description 之外的字段:description 按设计要保留(那是 Telegram 对
// 这次请求的说明),而「体里其余字节一个字都不拼」才是要钉住的口径。
func TestDoJSON_StatusErrorDoesNotEchoBodySentinel(t *testing.T) {
t.Parallel()
const sentinel = "SENTINEL-TG-BODY"
a, _ := newTestAdapter(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{"ok":false,"error_code":400,"description":"Bad Request: chat not found",` +
`"echoed_params":{"text":"` + sentinel + `"}}`))
}))
_, err := a.doJSON(context.Background(), "sendMessage", map[string]any{"chat_id": "999"})
if err == nil {
t.Fatal("400 却报成功")
}
if !errors.Is(err, ErrBadResponse) {
t.Fatalf("err = %v, want ErrBadResponse", err)
}
if strings.Contains(err.Error(), sentinel) {
t.Fatalf("响应体其余字段进了错误(会随 long poll failed 落日志):%v", err)
}
if !strings.Contains(err.Error(), "error_code 400") || !strings.Contains(err.Error(), "chat not found") {
t.Fatalf("error_code/description 丢了(诊断面不能一起丢):%v", err)
}
if !strings.Contains(err.Error(), "status 400") {
t.Fatalf("HTTP 状态码丢了:%v", err)
}
}
// TestDoJSON_StatusErrorRedactsDescriptionSecrets description 是上游自由文本,而
// setWebhook 失败时 Telegram 会把我们给的地址回显在里面 —— 地址的 query 常带凭据。
// 这类赋值形状必须被清洗。改回原样透传就变红。
func TestDoJSON_StatusErrorRedactsDescriptionSecrets(t *testing.T) {
t.Parallel()
const secret = "WEBHOOK-SECRET-VALUE"
a, _ := newTestAdapter(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{"ok":false,"error_code":400,"description":"bad webhook ` +
`https://example.com/hook?access_token=` + secret + `"}`))
}))
_, err := a.doJSON(context.Background(), "setWebhook", map[string]any{"url": "https://x"})
if err == nil {
t.Fatal("400 却报成功")
}
if strings.Contains(err.Error(), secret) {
t.Fatalf("description 里的凭据原样进了错误:%v", err)
}
}
// TestDoJSON_TransportErrorOmitsBotToken 传输层失败时,错误里不得出现 bot token ——
// 它就写在 Bot API 的地址路径里。
func TestDoJSON_TransportErrorOmitsBotToken(t *testing.T) {
t.Parallel()
const token = "123456:AAH-SECRET-BOT-TOKEN"
a := New(
Config{BotToken: token},
WithBaseURL("http://api.example.com"),
WithHTTPClient(&http.Client{Transport: failingTransport{}}),
WithLogger(discardLogger()),
)
_, err := a.doJSON(context.Background(), "getUpdates", nil)
if err == nil {
t.Fatal("传输层失败却报成功")
}
if strings.Contains(err.Error(), token) {
t.Fatalf("bot token 随地址进了错误(会随 long poll failed 落日志):%v", err)
}
if !strings.Contains(err.Error(), "api.example.com") || !strings.Contains(err.Error(), "connection refused") {
t.Fatalf("剥掉地址时把来源或原因一起丢了:%v", err)
}
}