Files
tamabox/outputs/p2-fixes-and-p0-alt-2026-09-01.md
T
2026-09-08 10:10:44 +08:00

6.7 KiB
Raw Blame History

TamaBox-v4 — P0 替代方案 + P2 整改(除 P2-4)与复审

  • 日期:2026-09-01
  • 范围:P0 IP 伪造的「免 CIDR」替代方案落地;P2-1P2-3、P2-5P2-14(除 P2-4)全部改进;改动后复审。
  • 方法:静态走查 + go build / go vet / go test(受 webp CGO 沙箱限制,含 internal/storage 的包仅做 gofmt -e 语法校验)。

一、P0 的「免 CIDR」替代方案(你最关心的点)

原报告建议「维护 Cloudflare 全量 CIDR 白名单 + 自右向左剥离跳数」。但正如你指出的:CIDR 列表太大、且 Cloudflare 边缘节点会变动,维护成本高且易遗漏。本次采用 不依赖任何 CDN CIDR 清单 的方案:

设计核心(internal/shared/ip.go)

  1. 优先采用 CF-Connecting-IP(Cloudflare 权威头)。只要源站被 锁定只接受 Cloudflare 连接,该头即等于真实客户端 IP,无需任何 IP 白名单。
  2. trusted_proxies 仅用于你自己的基础设施(如本机 nginx 127.0.0.1、私有 10.0.0.0/8 LB),默认空。仅当 TCP 对端命中该小清单时才解析 X-Forwarded-For / X-Real-IP,且从右向左剥离已知可信跳数后取原始客户端。
  3. 其余情况一律回退到 RemoteAddr,绝不信任客户端自行提供的头。

为什么这能根治,且不用 CIDR

  • 关键在部署侧锁定源站,推荐二选一(都不需要 CIDR 清单):
    • Cloudflare Tunnel(cloudflared):源站无任何公网 IP,只与 Cloudflare 建隧道 → 攻击者无法直连源站伪造 CF-Connecting-IP。
    • Cloudflare Authenticated Origin Pull(mTLS):源站 TLS 要求 Cloudflare 客户端证书 → 非 Cloudflare 连接被拒。
  • 二者都让「伪造 CF-Connecting-IP」在物理/密码学层面不可行,从而免去了维护庞大 CIDR 的负担。

配置变更

  • conf/static.go:Security 新增 TrustedProxies string \ini:"trusted_proxies"``。
  • conf/app.sample.ini:[security] 下新增 trusted_proxies =(默认空),并改写 trust_proxy 注释,明确「开启 trust_proxy 必须配合 Tunnel/mTLS 锁定源站」。
  • internal/cmd/web.go:conf.Init() 后调用 shared.InitTrustedProxies() 解析清单(解析失败则启动报错)。

残留风险(已文档化):若 trust_proxy=true 但源站仍可被直连(未锁源站),攻击者可自行发 CF-Connecting-IP 被信任。生产必须配合上述锁定手段。这是「免 CIDR」方案的必要前提。


二、P2 整改清单(除 P2-4 外全部完成)

# 位置 改动
P2-1 route/vote/vote.go:310 排序题统计 json.Unmarshal(s.FormData, &data.Ranking) → &data,修复解析必失败、排名恒为 0 的 bug
P2-2 route/vote/vote.go + route/user/vote.go 抽离 InvalidateResultsCache(voteID);除新提交外,草稿「选项/字段重建」「投票删除」也主动失效结果缓存
P2-3 internal/db/vote_submissions.go DeleteByVoteID 改为 Unscoped() 物理删除;避免软删行残留占据唯一索引导致「统计归零却无法重投」
P2-5 internal/db/questions.go + follow_ups.go + upload_images.go 删除提问时级联软删其 follow_ups 与 upload_images,消除孤儿记录
P2-6 internal/db/users.go:814 CountTodayMessagesFromUser 改用应用本地时区当日 00:00 边界,替代 DB 的 CURRENT_DATE,修正每日私信上限窗口偏移
P2-7 route/user/profile.go 导出 XLSX 单元格前置单引号防御公式注入(以 = + - @ \t \r \n 开头的内容按纯文本处理)
P2-8 internal/db/users.go:244 注册 Create 遇重复键错误时重新执行 validate 返回友好错误(ErrDuplicateEmail/ErrDuplicateDomain),消除 TOCTOU 暴露底层 DB 错误
P2-9 internal/route/gzip.go Write 将嗅探到的 Content-Type 写回响应头使压缩判定生效;204/304 不再附加 Content-Encoding
P2-10 internal/route/route.go:244 /i18n/{lang} 重定向拦截反斜杠 \\ 与控制字符,防开放重定向变体
P2-11 internal/route/route.go 加 Strict-Transport-Security(仅 https 时下发);lang cookie 在 https 下设 Secure
P2-12 internal/route/chat/chat_handler.go SendMessage 发送前重新校验 FeatureStateOf(FeatureChat).Open,箱主中途关闭私聊即阻止继续发消息
P2-13 internal/db/questions.go + route/question/page.go 置顶优先分页改用复合键集游标 (is_pinned, created_at, id),消除置顶项与非置顶项 id 交错导致的「加载更多」漏项
P2-14 internal/captcha/math.go 已由前次 P1-3 覆盖:rand.Int 错误已校验(无新增改动)

P2-4(Question.Token 仅 6 位/唯一索引)按你要求跳过。


三、复审验证结果

项目 结果
go build 不依赖 storage 的包(shared/db/captcha/security/censor) ✅ 全绿
go vet 同上包 ✅ 无告警
go test ./route/vote/... ✅ 通过(覆盖 P2-1 排名聚合、缓存接口一致)
gofmt -e 受 webp 影响的编辑文件(route/user/*、route/question/page.go、internal/route/*、chat_handler.go) ✅ 均无语法错误
route/vote 整包 go build ✅ 通过(P2-1/P2-2 改动类型校验通过)

⚠️ 含 internal/storage(webp CGO)的包(internal/route、route/user、route/question、internal/route/chat、cmd/web)本沙箱无法完整编译(既定限制,与改动无关)。这些文件已用 gofmt -e 确认无语法错误,需在 Linux / 带 libwebp 环境跑一次 go build ./... 做最终确认。

复审新发现 / 残留项

  • P2-2 多实例一致性:本方案让各实例在「新提交 / 改选项 / 删投票」时各自失效自身进程内缓存,但跨实例仍依赖 30s TTL 自然过期(进程内 TTL 缓存的固有限制)。若未来做多实例横向扩容,建议将 resultsCache 迁到 Redis 共享缓存,方能做到即时全局一致。当前单实例/低并发场景已足够。
  • P2-5 级联为软删除:与提问本身的软删除保持一致;对象存储中的实际图片文件未删除(避免误删复用资源),仅清 DB 记录。若需彻底清理文件,应另起异步任务。
  • 其余改动逻辑自洽,未发现引入新的越权 / 竞态 / 注入点。

四、待你确认/执行的部署动作

  1. P0 生效前提:生产环境用 Cloudflare Tunnel 或 Authenticated Origin Pull 锁定源站;并在 app.ini 设 trust_proxy = true、trusted_proxies 填入你自己的反代 IP(如 127.0.0.1)。
  2. 最终构建:在带 libwebp 的环境 go build ./... 验证全部包。