Files
tamabox/.workbuddy/memory/2026-10-23.md
T

156 lines
11 KiB
Markdown
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.
# 2026-10-23
## 单题分享卡二维码提到 5px/模块(`310f851`)
用户决定「单题一起做」,同时否掉方案 5(结果落盘/R2)——理由:功能使用率不高,不值得大动干戈。
- 单题卡卡片 210×255 放不下 5px 模块(37 模块要 185px):`QQRCardW` 236、`QQRCardH` 300、
`QQRSize` 190。X 由 `QContentX+QContentW-QQRCardW` 派生 → 679,Y 居中 → 120。
- `QTextMaxW` / `qLeftClip` 都从 `QQRCardX` 派生,左栏自动收窄 26px,无需另改。
- 抽了 `drawQuestionQRCard(canvas, user, question, logo, fontCtx)`(handler 只剩调用),
这样测试能直接渲染整卡:`TestQuestionQRCardLayout` 断言正方形 / 模块≥5 / 四边留白≥20 /
居中≤1px,`TEST_PREVIEW=1` 出 `outputs/sharecard-preview/question-card.png`。
- 实测:二维码 185px、居中偏差 0.5px、上下留白 34px;gozxing 在 960/720/480/320 全通过。
变异验证:`QQRSize` 改回 150 → "QR module is 4px, want >=5" FAIL。
- 未做(用户明确不要):分享卡结果落盘/上 R2。进程内 LRU 仍是 16 条 + TTL 1h,
冷渲染成本接受现状。
## 部署后复验(箱子卡 + 单题卡,`310f851`)
- 箱子卡:QR 185×185 @ y120-304 / x702-886,卡中心 795 → 着墨中心 794.0(−1.0px);
提示语 794.0、分隔线 795.0、链接两行 795.0 / 794.5;上下留白各 18px。
- 单题卡:QR 185×185 @ y154-338 / x704-888,卡中心 797 → 着墨中心 796.0(−1.0px);
提示语 797.0;上下留白 34 / 33px。
- 端到端解码(部署图,含真实背景与内嵌头像):两张卡在 960 / 720 / 480 / 320 宽全部 OK,
内容正确(单题卡 `/114`)。
- 顺带确认 **不是** bug:删除提问后分享卡立刻 302(`GetByID` 在缓存查询之前),
不存在「删了还能靠缓存取到图」的窗口。
## 缓存 key 排查结论(`675cecb`)
用户要求「有影响就修」。查下来**当前没有实际影响**,只有一条理论缺口:
| 可能场景 | 是否真有影响 | 原因 |
|---|---|---|
| 用户改域名 → 二维码指向旧域名 | ❌ 当前无 | Domain 只在注册时写(`register.go` / `CreateUser`),无改域名入口 |
| 改 ExternalURL / ShareBackground | ❌ 无 | 配置需重启,缓存是进程内的,重启即清 |
| 换头像/背景后仍是旧图 | ❌ 无 | R2 key = `picture/日期/随机 hex`,内容不可变,换图必换 URL |
| 域名将来可改 | ⚠️ 理论 | 单题卡 key 未含 `user.Domain` |
处理:给 `questionShareCardCacheKey` 补 `user.Domain`(箱子卡本来就有);
两处「TTL 30 分钟」注释改成 1 小时(代码一直是 `time.Hour`)。
新增 `TestShareCardCacheKeysCoverDrawnFields`——逐字段变异断言 key 必须变,
去掉 Domain 立刻 FAIL。**以后改分享卡要加新字段时,先看这张测试。**
## 正式环境「蜜罐封了但还能访问」排查(IPv6)
本地用 IPv6 全程复现了一遍 autoban 链路(CF-Connecting-IP 注入 + trust_proxy=true):
封禁前 200 → POST 凭据后 `IsBanned`=true → 同地址 403 → 同 /64 换地址仍 403 →
不同 /64 放行 200。**判定逻辑与中间件都正确,不是代码 bug。**
封禁键 = `shared.LimitKey(ResolveClientIP(req))`:
- IPv4 原样
- IPv6 折成 `xxxx:xxxx:xxxx:xxxx::/64`
写入(honeypot.Serve / route.NotFound)与检查(autobanMiddleware)用的是同一个函数,一致。
⇒ 「邮件说封了但能访问」只能是**键不一致**或**进程重启(封禁纯内存态)**。
三种可能:① 部署后重启过;② trust_proxy 未配 → 键是反代地址(`127.0.0.1` / `::/64`);
③ 前后请求地址族或 /64 不同(双栈、IPv6 前缀轮换、多实例)。
判定方法:`honeypot hits` / `data/honeypot-creds/creds-YYYY-MM-DD.jsonl` 里的 `ip` 字段
就是**被封的键**,与本机 `curl -6 ifconfig.co` 比对即可。
**发现的真实取证缺陷**:`db.HoneypotHit.IP` 与 creds 文件存的是折叠后的 `/64` 键,
IPv6 丢掉了精确地址(IPv4 无损);邮件里也只给键,运维会误以为「这不是我的 IP」。
建议:取证记精确 IP,封禁仍用 /64;建封与 403 命中各打一条日志。
### 据此动手改的三处(精确 IP / 封禁键分离 + 可观测性)
1. `honeypotHandler.Serve` 拆两个变量:`clientIP = ResolveClientIP`(精确,取证/邮件用)、
`banKey = LimitKey(clientIP)`(/64,封禁/采样/告警去重用)。DB 与凭据文件都写 clientIP。
2. `autoban.Options.OnBan(key, duration, reason)` 回调(新增 `ReasonProbe` / `ReasonHoneypot`),
保持 autoban 纯标准库、不再依赖 logrus。route.newBanner 注入 → 建封打 WARN
(ban_key / reason / duration / expires_at)。中间件 403 分支打 Debug。
3. 邮件同时给「来源 IP」(精确)与「封禁范围」(ban_key,仅 `ne .ip .ban_key` 时渲染);
顺手把模板里过时的「已入库」改成「写入取证文件」(上一轮凭据外置后没同步)。
副作用修掉:`honeypot stats` 的来源排行改按 `LimitKey` 合并(否则 IPv6 轮换地址会把排行碎成
几十条各 1 次)。SQL 取 `limit*4` 再合并,属近似。
测试:`TestHoneypotRecordsExactIPv6ButBansByPrefix`(精确 IP + /64 键 + store 断言)、
`TestOnBanCallback`、`TestHoneypotAlertTemplateRenders`、`TestMergeSourcesByKey`。
四处变异验证有效(改回存聚合键 / 封禁用精确地址 / 去掉 notifyBan / 不合并排行)。
**变异验证的坑**:把 `shared.LimitKey(r.IP)` 改成 `r.IP` 会因 `shared` 未使用而
**编译失败**(不是断言失败);变异要保证仍能编译(改成 `LimitKey(r.IP) + r.IP` 才测到逻辑)。
### 测试环境验收(部署 29ddee0 后)
伪造 `CF-Connecting-IP: 2001:db8:…`(RFC 文档保留段,封了不影响任何人)直连源站端口:
- 基线 200 → POST 凭据 200(假页面)→ **同 /64 换地址 403**(聚合封禁生效);
- 凭据文件 `ip` = `2001:db8:dead:beef::1234`(精确,无 /64 后缀),`banned:true`;
- 换另一个 /64(`2001:db8:cafe::1`)→ 200,说明封的是 /64 不是全局;
- `od -c` 复核 `"path":"/wp-login.php"` **无前导空格** → cleanPath 生效。
**教训**:上一次那条 `" /wp-login.php"` 是**那次请求的 URI 本身带空格**(本地实测
`Match(" /wp-login.php")=true` 靠 TrimSpace 命中、`Match("/ wp-login.php")=false`),
不是代码问题;判断这类"看不见的字符"一律用 `od -c`,别靠终端渲染。
测试环境服务是**前台跑在 /dev/pts/1**,日志不在文件里 —— 拿日志要 `tmux capture-pane` 或
下次启动加 `>> logs/tamabox.log 2>&1`。
### 蜜罐升级为 9 个伪装家族
新增 `family.go`(皮肤表)/ `asset.go`(静态资源占位)/ 重写 `page.go`:
- 按路径分发:wordpress / phpmyadmin / tomcat(401+Basic) / jenkins / grafana / gitlab /
luci / joomla / generic;每类独立字段名、配色、指纹响应头、Set-Cookie;
- `credentialKeys` 扩 `pma_*` / `j_*` / `luci_*` / `user[login]`;
`token` 只用于判 HadFields,**不进 isPasswordKey**(否则 PMA 的 hidden token 会随机顶掉真密码);
- 静态资源(`/wp-admin/css/…` 等)回占位内容,**不落库不计数** —— 一次页面加载 5–10 个请求;
- `honeypot paths` 加家族汇总(实测 110 条:generic 84 / wordpress 10 / tomcat 6 / gitlab 3 / pma 3 …);
- 性能:RenderFamily 12µs→3.7µs、22→2 allocs、19.5KB→5.2KB(禁 `"a"+b+"c"`、Replacer 提包级)。
**新工具** `dev/preview-honeypot`:渲染 9 个家族到 `outputs/honeypot-preview/index.html`,
配 chrome headless 截图核对外观。靠它发现两处「源码看不出、截图一眼假」:
Tomcat 的 skin 忘了 css(按钮白字无底色)、WP 登录页底部挂着 404 文案。
### 测试环境验收:9 家族伪装(部署 7225f68 后,只读 GET)
全部符合预期:8 个家族 title/generator 各不相同、Tomcat 401+Basic realm、
静态资源 200 且 MIME 正确(128B css / 81B js / 70B png)、插件目录 .php 回 404、
业务路由(/login /user /static /api)无 canary、界外路径仍 302 → /。
发现的漏洞(按优先级):
1. **P1 `/xmlrpc.php` 回 HTML 登录页** —— 而我们自己发了 `X-Pingback: /xmlrpc.php`,
真实 WP 回 text/xml,等于自己指给自己一个明显假的地址;同时丢掉 WP 爆破最有价值的
捕获通道(xmlrpc system.multicall 一次可试上千组,绕开登录限速)。
2. **P2 每次请求恒定值**:PMA `token=e1a4c0b7d2f58g93`(还带非 hex 的 g)、
generic `csrf_token=a7f3c1e9b2d4`、`X-Jenkins-Session: 8b1f2c4d`。
两次请求完全相同 → 跨请求指纹即可识别。应改每请求随机。
3. **P2 `X-Pingback` 用相对路径**,真实 WP 给绝对 URL。
4. **P3 `/phpinfo.php` 漏匹配**:路径清单里是 `/phpinfo`、`/info.php` 精确项,
段前缀匹配不覆盖 `.php` 变体 → 302。可加「p 去掉 .php 后在集合内」的兜底。
5. **P3 `/wp-json/wp/v2/users` 回 HTML**,真实 WP 回 JSON(用户枚举接口)。
6. P3 `X-Frame-Options: DENY` / `Cache-Control: no-store` 与真实 WP 不同,
但属全局中间件,改动影响全站。
### 修 P1/P2:XML-RPC 端点 + 动态 token + .php 兜底
- 新增 `internal/security/honeypot/xmlrpc.go`:`/xmlrpc.php` 不再回登录页。
GET→405+XML fault「XML-RPC services are disabled」;POST methodCall→200+fault
「Incorrect username or password」;POST system.listMethods→方法列表模板。
凭据提取用正则(不上 encoding/xml,蜜罐高频命中):抓 `<string>`、剔除
methodName 与 http(s) URL、两两配对;`blog_id=1` 这种「奇数长度+首项短数字」
会丢首项纠正错位;第一组进标准字段,其余进 `cred2.*`,原始串全量进 `xmlrpc.N`
(配置上限 16 对 / raw 8KB)。
- 动态占位符:`LoginPageVars.Nonce`(32 hex,`math/rand/v2`,刻意不用 crypto/rand,
这些是假 token 只需「每次不同」)。支持 `$hex8/16/32` 与 `$base`;
页头、hidden、Set-Cookie 共用同一套 nonce。
- `X-Pingback` 改绝对地址(`$base/xmlrpc.php`),没配 `external_url` 就不发。
- `Cache-Control` 从 `no-store` 改 `no-cache, must-revalidate, max-age=0`(更像 WP/PMA)。
- Grafana/GitLab 补真实会种的会话 cookie(`grafana_sess`、`_gitlab_session`,动态)。
- `Match` 增加 `.php` 变体兜底:`/phpinfo.php` 之前漏匹配 → 302。
- 性能:RenderFamily 3.7µs→4.18µs、2→3 allocs(多的一次是 hex 编码),可接受。
四处变异验证有效(固定 token / xmlrpc 分支失效 / Pingback 相对路径 / .php 兜底失效)。