6.0 KiB
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 项)
- mail.go:123:
SetHeader("To", to)建议SetAddressHeader(防 header 注入的纵深防御)。 - page.go:664/673:上传 key 保留用户可控后缀(可为 .svg/.html),建议后缀白名单。
- 孤儿文件:图片上传 S3 成功但 DB 失败仅记日志;删除从不清理 S3 对象 → 存储只增不减。
- conf/db.go:16:PostgresDsn 密码未转义 +
sslmode=disable写死。 - dbutil/pagination.go:19:
LimitOffset无 pageSize 上限,可被超大值打 DB。 - storage/oss.go:全项目已无调用方(遗留代码),建议删除。
- autoban 双计数:敏感路径未命中路由时被中间件和 f.NotFound 各计一次 → 封禁提前触发;NotFound 应跳过 IsSensitivePath 命中的路径。
- captcha VerifyOnly:Attempts 读改写非原子,并发可绕 5 次上限(单会话低风险)。
- 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 择要。