fix(honeypot): cleanPath 处理零宽字符,并在清洗生效时打诊断日志

上一版只用 strings.TrimSpace,实测漏掉「看不见但不算 White_Space」的字符:
  cleanPath("\u200b/wp-login.php") = "/\u200b/wp-login.php"
(零宽没被删,还因为首字符不是 '/' 多补了一个斜杠,拼出更怪的路径)

改为 TrimFunc 同时匹配 unicode.IsSpace 与 U+200B/200C/200D/FEFF,且只处理首尾
—— 中间的零宽字符可能是攻击 payload,取证要保留原样。

另加诊断日志:清洗前后不一致时 WARN,用 %q 打出原始值。
测试环境出现过「记录里 path 带前导空白、本地链路却复现不出来」的情况,
不可见字符必须转义输出才定位得到。

测试:TestCleanPath 补零宽空格 / BOM / 中间零宽字符保留三个用例。
This commit is contained in:
tamakyi
2026-10-21 16:18:25 +08:00
parent 846bb8a3d9
commit 74312fba58
4 changed files with 56 additions and 10 deletions
+26 -3
View File
@@ -6,10 +6,12 @@ package route
import (
"context"
"fmt"
"net/http"
"strings"
"sync"
"time"
"unicode"
"github.com/flamego/flamego"
"github.com/sirupsen/logrus"
@@ -135,6 +137,15 @@ func (h *honeypotHandler) Serve(ctx flamego.Context) {
// 但落库与假表单的 action 用的是原始值 —— 会留下脏取证数据,
// 甚至让爆破脚本把表单提交到错误路径。这里统一清洗一次。
path := cleanPath(req.URL.Path)
// 诊断:清洗过说明上游注入了空白/零宽字符。用 %q 打出来 ——
// 这类字符肉眼不可见,裸打印会看不出哪里不同(曾经为此排查了一轮)。
if raw := req.URL.Path; raw != path {
logrus.WithFields(logrus.Fields{
"ip": ip,
"raw": fmt.Sprintf("%q", raw),
"used": fmt.Sprintf("%q", path),
}).Warn("蜜罐路径含上游注入的空白/零宽字符,已清洗后使用")
}
// 来源 IP 是回环地址 + 未开 trust_proxy,是「站点在反代后面但没配信任」的
// 典型信号。此时所有访客在源站眼里都是同一个 IP:
@@ -184,11 +195,23 @@ func (h *honeypotHandler) Serve(ctx flamego.Context) {
h.record(ip, req, path, creds, banned)
}
// cleanPath 去掉路径首尾空白并保证前导斜杠。
// cleanPath 去掉路径首尾的空白与零宽字符,并保证前导斜杠。
//
// 刻意不改大小写:URL 路径是大小写敏感的,取证必须保留原始形态。
// 为什么不能只用 strings.TrimSpace:它只认 Unicode White_Space,
// 而 U+200B(零宽空格)/ U+200C / U+200D / U+FEFF(BOM)都属于「看不见但不算空白」
// 的字符 —— 反代或某些 WAF 改写 URI 时可能注入它们,TrimSpace 会原样放过,
// 甚至因为首字符不是 '/' 而额外补一个斜杠,拼出 "/\u200b/wp-login.php" 这种怪路径。
//
// 刻意只处理首尾:中间的零宽字符可能是攻击 payload 的一部分,取证要保留原样。
// 也刻意不改大小写:URL 路径大小写敏感。
func cleanPath(p string) string {
p = strings.TrimSpace(p)
p = strings.TrimFunc(p, func(r rune) bool {
switch r {
case '\u200b', '\u200c', '\u200d', '\ufeff':
return true
}
return unicode.IsSpace(r)
})
if p == "" {
return "/"
}
+10 -7
View File
@@ -81,13 +81,16 @@ func newHoneypotTestServerFull(t *testing.T, enabled, autoBan bool, cs honeypot.
// 取证数据变脏,爆破脚本还可能把表单提交到错误路径。
func TestCleanPath(t *testing.T) {
cases := map[string]string{
"/wp-login.php": "/wp-login.php",
" /wp-login.php": "/wp-login.php",
"\t/wp-login.php": "/wp-login.php",
"wp-login.php": "/wp-login.php",
"": "/",
" ": "/",
"/ADMIN": "/ADMIN", // 不改大小写:URL 路径大小写敏感
"/wp-login.php": "/wp-login.php",
" /wp-login.php": "/wp-login.php",
"\t/wp-login.php": "/wp-login.php",
"wp-login.php": "/wp-login.php",
"": "/",
" ": "/",
"/ADMIN": "/ADMIN", // 不改大小写:URL 路径大小写敏感
"\u200b/wp-login.php": "/wp-login.php", // 零宽空格(TrimSpace 不认)
"\ufeff/wp-login.php": "/wp-login.php", // BOM
"/wp\u200b-login.php": "/wp\u200b-login.php", // 中间的零宽字符保留(可能是 payload)
}
for in, want := range cases {
if got := cleanPath(in); got != want {