Files
tamabox/outputs/re-audit-2026-09-01.md
T
tamakyi e3e59e7c69 chore: baseline snapshot before moderation-strictness feature
Repo .git was damaged (refs/ missing, index truncated, object store near-empty).
Repaired via git init (rebuilt refs), stale index preserved as .git/index.corrupt-20260910.bak,
then full worktree committed as a rollback baseline.
2026-09-10 10:56:27 +08:00

6.0 KiB
Raw Blame History

TamaBox-v4 第三轮审计报告(2026-09-01)

本轮聚焦前两轮未覆盖的区域(上传/存储/邮件/基础设施、模板层)+ 对 8/31~9/1 修复批次做回归复审。 所有 P0/P1 结论均已逐一核对源码确认,并纠正了子代理的一处误报(见 P1-3 说明)。

验证基线

  • go build:不依赖 storage 的 14 个包全部通过;go test(form/i18n/cache/autoban/censor)全绿。
  • ⚠️ 注意:本轮两个 P0 都藏在 webp 屏障遮住的包里(route/user、route/db),Windows 下编译不到——务必在 Linux/带 libwebp 环境跑一次 go build ./...,否则下面 P0-1 会直接让部署失败。

🔴 P0(2 项,均为上一批修复引入的回归)

P0-1 profile.go:335 — 导出函数无法编译(构建失败)

personalData 是 [][]interface{},sanitizeCell(row[0]) 把 interface{} 传给 string 参数 → 编译错误。webp CGO 屏蔽了本机报错,Linux/Docker 构建同样会失败。 修复:遍历单元格时先做 s, ok := v.(string) 类型断言,非字符串(如 user.CreatedAt 的 time.Time)跳过净化原样写入。

P0-2 history-template.html:29 — 复合游标未同步,「加载更多」翻页损坏

P2-13 把游标改成 <isPinned>.<createdAtNano>.<id> 复合格式,但前端 JS 仍回传纯 data[data.length-1].id → 服务端 parsePinCursor 解析失败 → 游标被忽略 → 每次都返回第一页(内容重复、无法翻页)。 修复:JS 按 ${q.is_pinned?1:0}.${new Date(q.created_at).getTime()*1e6}.${q.id} 组装游标;或服务端兼容裸 id 格式(解析失败时回退为 id 游标)。


🟠 P1(6 项)

P1-1 upload_images.go:167 — 级联删除查询了不存在的列

DeleteByQuestionID 在 upload_images 表上执行 WHERE question_id=?,但该列实际在关联表 upload_image_questions(:54)。PG 报列不存在 → 级联失败(目前仅记日志不阻断删除),关联记录永久残留。 修复:改为先 UploadImageQuestion 按 question_id 查出 imageID 列表,再软删对应 UploadImage(并顺带删 link 行)。 (纠正误报:子代理称"提问无法删除",实际 questions.DeleteByID 只记日志不返回错误,删除本身仍成功。)

P1-2 conversation.go:205 — 重复删除导致配额二次回退

会话删除是标志位软删(is_deleted_by_owner),重复 POST 同一删除请求,First() 仍命中 → 每次都 IncrementQuota(-1)。守卫保证了不为负,但多条会话反复删可把计数错误清零。 修复:First() 后若 conv.IsDeletedByOwner == true 直接幂等返回。

P1-3 questions.go:222 — 非 PinFirst 游标路径在 PG 下报错 + ASC 方向错误

q.Where("id < ?", cursorStr) 传 string,PG 中 integer < text 无操作符 → 管理后台 /api/.../questions 带游标请求 500。且 ASC(sort=oldest)时仍用 id < ?,方向相反 → 翻页停在第一页。 修复:strconv.ParseUint 校验游标;按 opts.SortOrder 选择 id < ?(DESC)或 id > ?(ASC)。

P1-4 home.html:8-12 — 远程内容经 x-html 无消毒渲染(供应链 XSS)

首页从外部 API(api-direct.337845818.best/...importantinfomation.json)取 log.content 后 x-html 直出。该域被劫持或 JSON 中混入用户上报内容时,首页即任意 JS 执行。 修复:改 x-text(内容纯文本时),或引入 DOMPurify 白名单过滤后再 x-html;顺带补上 class=uk-article-meta 缺失的引号。

P1-5 tracing/flamego.go:42 — span 名含 query string,私密 token 落日志

span 取 RequestURI,?t=<问题token>、找回密码 ?code= 等会原样写入 trace;配合 AlwaysSample + stdout 全量输出,1C1G 上还是 IO 负担。 修复:span 名只用 URL.Path;生产关闭 stdout exporter 或降采样。

P1-6(遗留提醒)password.go — legacy 密码 == 非常量时间比较

第一轮报告的 P1-6 至今未修(你当时未勾选)。bcrypt 失败静默降级 HMAC 路径同样在。建议尽快处理。


🟡 P2(择要 9 项)

  1. mail.go:123:SetHeader("To", to) 建议 SetAddressHeader(防 header 注入的纵深防御)。
  2. page.go:664/673:上传 key 保留用户可控后缀(可为 .svg/.html),建议后缀白名单。
  3. 孤儿文件:图片上传 S3 成功但 DB 失败仅记日志;删除从不清理 S3 对象 → 存储只增不减。
  4. conf/db.go:16:PostgresDsn 密码未转义 + sslmode=disable 写死。
  5. dbutil/pagination.go:19:LimitOffset 无 pageSize 上限,可被超大值打 DB。
  6. storage/oss.go:全项目已无调用方(遗留代码),建议删除。
  7. autoban 双计数:敏感路径未命中路由时被中间件和 f.NotFound 各计一次 → 封禁提前触发;NotFound 应跳过 IsSensitivePath 命中的路径。
  8. captcha VerifyOnly:Attempts 读改写非原子,并发可绕 5 次上限(单会话低风险)。
  9. questions 删除链路非事务:级联失败仅记日志可留孤儿,建议包 Transaction;另 sanitizeCSVCell(vote.go:1194)缺 \t\r\n 分支,与 profile.go 净化规则不一致。

✅ 本轮确认无问题的部分

  • 模板层:41 处 POST 表单 CSRF token 全覆盖;toJSON/内联 JS 均经 html/template script-context 转义,无逃逸;Markdown 白名单消毒;探索页 AJAX 分支 escapeHTML 正确。
  • storage/s3.go:LimitReader 8MB + 魔数 + 像素上限 + 随机 key + 重试,凭证不入日志。
  • cache:RWMutex + maxItems + TTL,无泄漏。
  • 回归确认正确:XFF 从右向左剥离、autoban 单实例单次 Next、配额归属(提问→箱主、私聊→OwnerID 一致)、追问服务端校验、proxy 编码绕过防护、pixel GET-only、chat 重校验覆盖全部写路径。

建议修复顺序

P0-1/P0-2(我引入的回归,必修)→ P1-1/P1-2/P1-3(数据一致性)→ P1-4/P1-5 → P1-6 遗留项 → P2 择要。