Files
tamabox/outputs/p2-fixes-and-p0-alt-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

77 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 ./...` 验证全部包。