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.
141 lines
7.0 KiB
Markdown
141 lines
7.0 KiB
Markdown
# TamaBox-v4 修复后代码复审报告
|
||
|
||
- **复审时间**:2026-08-26
|
||
- **审阅对象**:TamaBox-v4
|
||
- **复审基线**:`outputs/TamaBox-v4-code-review-report-2026-08-26.md`
|
||
- **复审范围**:上一轮 4 项确认问题的修复调用链、相关数据库模型、模板与回归测试;并对修复影响面做定向静态复查。
|
||
- **结论口径**:本报告仅将能由当前代码直接确认的问题列为确认问题;环境限制与建议性事项单独列出。
|
||
|
||
## 一、复审结论
|
||
|
||
上一轮报告中的 4 项确认问题均已完成代码级修复,未发现原问题仍可沿原路径复现:
|
||
|
||
| 原问题 | 复审结论 | 关键位置 |
|
||
|---|---|---|
|
||
| P1-01 匿名 `vote_fp` Cookie 可伪造 | **已修复** | `route/vote/vote.go:62-120, 691-700` |
|
||
| P1-02 Markdown 实体编码导致 XSS | **已修复** | `internal/template/markdown.go:143-147` |
|
||
| P2-01 多选重复选项可重复计票 | **已修复** | `route/vote/vote.go:232-247, 749-775, 802-809` |
|
||
| P2-02 投票次数配置与一人一票约束矛盾 | **已修复** | `route/user/vote.go:199-202, 563-566`、`templates/user/vote-form.html:85-88` |
|
||
|
||
本轮复审另发现并修复了一个由 P1-01 修复引入的兼容性问题:签名 token 长度为 108 字符,原数据库字段 `size:64` 不足,已调整为 `size:128`(`internal/db/vote_submissions.go:54-55`)。
|
||
|
||
**当前代码级结论:未发现 P0/P1/P2 确认问题。**
|
||
|
||
## 二、逐项复审证据
|
||
|
||
### 1. P1-01:匿名指纹 Cookie 伪造与防刷绕过
|
||
|
||
**状态:已修复。**
|
||
|
||
- `newVoterToken` 生成 32 字节随机 nonce,格式为 `64 位十六进制 nonce + "." + HMAC-SHA256 签名`。
|
||
- `signVoterToken` 使用 `conf.Server.XSRFKey` 作为服务端密钥;配置初始化已强制要求该密钥非空。
|
||
- `validVoterToken` 检查分段数量、nonce 长度、十六进制格式,并使用 `hmac.Equal` 比较签名。
|
||
- `ensureVoterToken` 对无效/伪造 Cookie 重新签发;`currentVoterToken` 对无效 Cookie 返回空值,不再将客户端任意字符串作为身份依据。
|
||
- 匿名提交无论是否带有效 Cookie,均按投票维度执行 24 小时 IP 计数;当前一人一票策略下,同一投票同一 IP 窗口内最多接受一次匿名提交。
|
||
- 数据库 `UNIQUE(vote_id, voter_token)` 仍作为有效 token 的最终并发防重兜底。
|
||
|
||
**残余设计取舍**:IP 兜底会对 NAT、校园网、公司网络等共享出口产生误伤;这是保守防刷策略,不是身份伪造漏洞。若业务必须支持共享出口多人投票,应改用验证码、设备/行为风控或更细粒度限流,而不能取消服务端限流。
|
||
|
||
### 2. P1-02:Markdown 实体编码 XSS
|
||
|
||
**状态:已修复。**
|
||
|
||
- `sanitizeHTML` 的 `html.TextToken` 分支现在对 tokenizer 已解码的文本调用 `html.EscapeString` 后再写入 `template.HTML`。
|
||
- 因此 `<script>`、双重编码输入不会在重建 HTML 时重新变成可执行标签。
|
||
- 标签、属性、URL 白名单逻辑保持有效;事件属性和危险协议仍被过滤。
|
||
- 回归测试已覆盖实体编码、双重编码、script、事件属性、危险 URL、iframe、SVG、style 以及 font color 属性注入。
|
||
|
||
### 3. P2-01:多选重复 option ID 重复计票
|
||
|
||
**状态:已修复。**
|
||
|
||
- `parseMultipleOptionIDs` 统一负责多选参数解析。
|
||
- 解析时使用 `seen` 集合;同一 `option_id` 出现两次直接返回“选项不能重复选择”。
|
||
- 同时校验空提交、最大选项数、选项归属和 `uint` 溢出,避免异常数值绕过校验。
|
||
- `LoadResults` 的多选聚合层也对历史数据中的重复 option ID 做防御性去重,因此旧异常数据不会在每次结果计算中再次放大统计。
|
||
- 新增测试覆盖正常多选、重复选项、超出上限、无效选项和整数溢出。
|
||
|
||
### 4. P2-02:MaxVotesPerFingerprint 与数据库约束矛盾
|
||
|
||
**状态:已修复。**
|
||
|
||
- 当前数据模型明确采用一人一票:登录用户由 `UNIQUE(vote_id, user_id)` 约束,匿名用户由 `UNIQUE(vote_id, voter_token)` 与 IP 兜底共同保护。
|
||
- 管理端解析不再信任旧页面或手工请求提交的 `max_votes_per_fingerprint`,统一按 `1` 保存。
|
||
- 新建/编辑失败回填也固定显示 `1`;表单输入设置为 `readonly`、`max="1"`,避免继续传达可配置多票语义。
|
||
- 四种语言的字段文案已改为明确的一人一票含义。
|
||
|
||
## 三、本轮新增兼容性问题及修复
|
||
|
||
签名 token 的组成长度为:64 字符 nonce + 1 个分隔符 + 43 字符 Raw Base64URL HMAC = **108 字符**。复审过程中确认原字段:
|
||
|
||
```go
|
||
VoterToken *string `gorm:"size:64;..."`
|
||
```
|
||
|
||
不足以存储完整 token,可能导致 MySQL 严格模式下匿名提交失败或 token 截断。现已改为:
|
||
|
||
```go
|
||
VoterToken *string `gorm:"size:128;uniqueIndex:idx_submission_token"`
|
||
```
|
||
|
||
并加入 token 长度回归断言。部署时依赖启动阶段的 GORM AutoMigrate 将字段扩容;上线前应在预生产数据库确认迁移结果和唯一索引状态。
|
||
|
||
## 四、验证结果
|
||
|
||
### 通过
|
||
|
||
```text
|
||
gofmt -d route/vote/vote.go route/vote/vote_stats_test.go route/user/vote.go \
|
||
internal/db/vote_submissions.go
|
||
# 无输出,格式通过
|
||
|
||
go test -count=1 ./route/vote ./internal/template ./templates ./internal/i18n \
|
||
./internal/security/autoban ./internal/security/censor ./internal/imagecheck \
|
||
./internal/shared ./internal/cache
|
||
# 全部 PASS
|
||
|
||
go vet ./route/vote ./internal/template ./templates ./internal/i18n
|
||
# 无输出,检查通过
|
||
```
|
||
|
||
### 全量验证限制
|
||
|
||
```text
|
||
go test ./...
|
||
go vet ./...
|
||
```
|
||
|
||
未能全绿,原因是当前环境已有的基础设施限制,不是本轮修复代码引入:
|
||
|
||
1. `github.com/chai2010/[email protected]` 在当前 Windows 环境缺少 CGO/WebP 编译符号(如 `webpGetInfo`、`webpDecodeRGB`),导致 `internal/storage`、`route/question`、`route/user`、主程序等相关包无法完整编译。
|
||
2. `internal/db` 测试未配置数据库类型,测试初始化报 `Unknown database type: ""`。
|
||
|
||
因此仍需在生产同构的 Linux/CGO 构建环境,并配置测试数据库后执行:
|
||
|
||
```text
|
||
go test ./...
|
||
go vet ./...
|
||
govulncheck ./...
|
||
```
|
||
|
||
## 五、上线前建议
|
||
|
||
1. 在预生产数据库执行/确认 AutoMigrate,确认 `vote_submissions.voter_token` 已扩容到至少 128 字符,且唯一索引仍存在。
|
||
2. 使用两个不同匿名客户端验证:同一 IP 同一投票第二次提交被拦截;伪造 `vote_fp` 会被覆盖且不能绕过 IP 限制;正常匿名首次投票成功。
|
||
3. 用真实浏览器验证实体编码文本仅以文字显示,不执行脚本;覆盖提问、回答、投票描述和结果页等 Markdown 输出点。
|
||
4. 若站点存在大量共享出口,结合验证码或行为风控评估 IP 一票策略的误伤率。
|
||
|
||
## 六、修改文件清单
|
||
|
||
- `internal/template/markdown.go`
|
||
- `internal/template/markdown_test.go`
|
||
- `route/vote/vote.go`
|
||
- `route/vote/vote_stats_test.go`
|
||
- `route/user/vote.go`
|
||
- `templates/user/vote-form.html`
|
||
- `internal/db/vote_submissions.go`
|
||
- `internal/i18n/dict_zh_cn.go`
|
||
- `internal/i18n/dict_zh_tw.go`
|
||
- `internal/i18n/dict_en.go`
|
||
- `internal/i18n/dict_ja.go`
|