接 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 凭据;八处变异逐条验证变红。
72 lines
2.6 KiB
Go
72 lines
2.6 KiB
Go
package io
|
||
|
||
// 本文件钉住 http 工具的**拦截日志**(M41 #16):rawURL 由模型/用户给,可能带
|
||
// `user:pass@` 或 `?api_key=`,而拦截日志是审计面、会被采集与转贴。旧实现把 rawURL
|
||
// 原样记进 WARN,等于把凭据写进日志。
|
||
//
|
||
// 现在只记来源主机与原因码;错误原文也不进日志 —— 解析失败时它包着 *url.Error,
|
||
// 里面就是整条原串。
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"errors"
|
||
"strings"
|
||
"testing"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||
)
|
||
|
||
// blockedLog 返回一个把日志写进 buffer 的 http 工具。
|
||
func blockedLog(t *testing.T) (*httpTool, *bytes.Buffer) {
|
||
t.Helper()
|
||
buf := &bytes.Buffer{}
|
||
return httpToolForTest(t, WithLogger(logger.New(logger.Options{Out: buf, Level: "debug"}))), buf
|
||
}
|
||
|
||
// TestHTTPTool_BlockedLogHidesURLCredentials SSRF 拦截时日志只留来源与原因码。
|
||
func TestHTTPTool_BlockedLogHidesURLCredentials(t *testing.T) {
|
||
t.Parallel()
|
||
ht, logs := blockedLog(t)
|
||
|
||
_, err := ht.Execute(context.Background(), map[string]any{"url": "https://user:pass@127.0.0.1/x"})
|
||
if !errors.Is(err, ErrSSRF) {
|
||
t.Fatalf("err = %v, want ErrSSRF", err)
|
||
}
|
||
logged := logs.String()
|
||
if !strings.Contains(logged, "io http: blocked request") {
|
||
t.Fatalf("没有拦截日志,测试没覆盖到目标路径:\n%s", logged)
|
||
}
|
||
if strings.Contains(logged, "user:pass") || strings.Contains(logged, "pass@") {
|
||
t.Fatalf("user:pass@ 进了拦截日志:\n%s", logged)
|
||
}
|
||
if !strings.Contains(logged, "reason=ssrf_blocked") {
|
||
t.Fatalf("拦截日志缺原因码:\n%s", logged)
|
||
}
|
||
if !strings.Contains(logged, "[redacted]@127.0.0.1") {
|
||
t.Fatalf("拦截日志缺来源主机(诊断面不能一起丢):\n%s", logged)
|
||
}
|
||
}
|
||
|
||
// TestHTTPTool_BlockedLogHidesQueryCredentials 非 SSRF 的拒绝(这里是不支持的 scheme)
|
||
// 同样不得带出 query 里的凭据。
|
||
func TestHTTPTool_BlockedLogHidesQueryCredentials(t *testing.T) {
|
||
t.Parallel()
|
||
ht, logs := blockedLog(t)
|
||
|
||
_, err := ht.Execute(context.Background(), map[string]any{"url": "ftp://user:pass@files.example.com/x?api_key=QUERY-SECRET"})
|
||
if err == nil {
|
||
t.Fatal("不支持的 scheme 却报成功")
|
||
}
|
||
logged := logs.String()
|
||
if !strings.Contains(logged, "io http: blocked request") {
|
||
t.Fatalf("没有拦截日志,测试没覆盖到目标路径:\n%s", logged)
|
||
}
|
||
if strings.Contains(logged, "QUERY-SECRET") || strings.Contains(logged, "user:pass") {
|
||
t.Fatalf("拦截日志带出了原串里的凭据:\n%s", logged)
|
||
}
|
||
if !strings.Contains(logged, "reason=url_rejected") {
|
||
t.Fatalf("拦截日志缺原因码:\n%s", logged)
|
||
}
|
||
}
|