# 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 把游标改成 `..` 复合格式,但前端 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 择要。