# TamaBox-v4 — P0 替代方案 + P2 整改(除 P2-4)与复审 - **日期**:2026-09-01 - **范围**:P0 IP 伪造的「免 CIDR」替代方案落地;P2-1~P2-3、P2-5~P2-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 ./...` 验证全部包。