13 KiB
13 KiB
TamaBox-v4 整体功能与逻辑审阅报告
- 审阅日期:2026-08-31
- 范围:全仓库(route / internal / templates / conf)
- 方法:静态走查 + 编译验证(
go build/go vet/ 单元测试)+ 对每项高危结论做源码核对(trust but verify) - 结论概览:核心架构扎实(路由/中间件/会话固定/回收令牌/配额原子递增/功能开关 Evaluate 逻辑均正确)。发现 1 个 P0 级安全缺陷(IP 伪造绕过)、若干 P1 逻辑/安全/可用性问题、一批 P2 健壮性项。无导致编译崩溃的致命代码错误。
0. 测试与构建验证结果
| 项目 | 结果 |
|---|---|
go version |
go1.25.5 windows/amd64 |
go build ./... |
❌ 仅因 internal/storage/s3.go 依赖 github.com/chai2010/webp(CGO 缺 libwebp 本机库,沙箱限制,与既有约定一致)。其余包均可独立编译 |
| 核心包编译 | ✅ internal/form internal/db internal/cache internal/i18n internal/captcha internal/security/* internal/context internal/conf internal/dbutil 全部 OK |
go vet(上述包) |
✅ 无告警 |
| 单元测试 | ✅ internal/form internal/i18n internal/cache internal/security/autoban 全部通过 |
| DB 集成测试 | ⚠️ 需外部 PostgreSQL/MySQL(DB_TYPE 环境变量,沙箱无),未执行;feature_control_test 等需落库测试建议在有 DB 环境补跑 |
说明:
gofmt -l发现 11 个文件未格式化(如internal/security/censor/text.go、internal/captcha/math.go、route/user/harassment.go等),建议gofmt -w统一。
🔴 P0 — 安全/可用阻断级
P0-1 反向代理 IP 信任可被伪造,限流/黑名单/自动封禁全线失效
- 位置:
internal/shared/ip.go:42-69(ResolveClientIP)、被internal/route/ratelimit.go、autoban.go、ipblacklist.go共用 - 问题:当
conf.Security.TrustProxy=true时,直接信任客户端发来的CF-Connecting-IP/X-Real-IP/X-Forwarded-For,不校验RemoteAddr是否属于可信代理 CIDR。任何人都可伪造这些头。 - 影响:
- 限速、IP 黑名单被完全绕过(每次请求换一个伪造 IP);
- 自动封禁(fail2ban)被反射利用——攻击者用伪造头把任意受害者 IP 打进黑名单,造成误封(含 CDN 出口 IP,可能误伤全站访客)。
- 现实性:
conf/app.sample.ini明确建议"站点在 Cloudflare 后且需自动封禁时必须设trust_proxy = true"——这正是绝大多数生产部署,故该缺陷在生产可达。 - 修复:配置
trusted_proxiesCIDR 列表;仅当RemoteAddr命中可信网段才读取代理头;X-Forwarded-For自右向左剥离已确认的可信跳数后再取最左原始 IP。
🟠 P1 — 逻辑错误 / 漏洞 / 可用性
P1-1 会话 Cookie 缺 Secure / SameSite
- 位置:
internal/route/route.go:92-95(session.Options仅设Initer/Config,无Cookie字段) - 影响:生产经 TLS 反代时,会话 Cookie 不会标
Secure(明文链路可泄露);未显式SameSite。conf.App.Production只切 Flamego 环境,不联动 Cookie 标志。 - 修复:按
conf.App.ExternalURL是否https设Secure:true;SameSite:http.SameSiteLaxMode(与既有 CSRF Token 协同);HttpOnly:true。
P1-2 配置解析:行内 ; 注释污染值,最坏导致启动崩溃
- 位置:
internal/conf/conf.go:25-27(IgnoreInlineComment:true)+conf/app.sample.ini:5,11,12,16-28,54 - 问题:该选项下
key = val ; 注释的注释会被并入字段值。示例里trust_proxy = false ; 是否信任…、ban_threshold_sensitive = 10 ; …等带行内注释。若把 sample 直接当app.ini用,File.Section("security").MapTo(&Security)解析 bool/int 会因尾部中文注释失败 → 进程启动即崩溃;share_background这类字符串字段则会得到带注释的非法 URL。 - 修复:将
IgnoreInlineComment改为false(安全默认,;重新开始注释),或全量清除 sample 中的行内注释(当前 sample 与其自身约定"禁用行内注释"矛盾)。
P1-3 本地算数验证码可被脚本自动解出 + 端点无节流
- 位置:
internal/captcha/math.go:195-212(GenerateHandler无限流)、:148-159(VerifyOnly不消费、不计数)、:49-55(忽略rand.Int错误) - 问题:验证码题目由服务端返回给客户端(如
"23 × 7 ="),答案对客户端完全可见,任何脚本都能读题即算——对机器人无实质阻挡;/api/captcha/verify无节流且VerifyOnly不消费,可被无限轮询;GenerateHandler无限流会导致会话条目无界膨胀(DoS)。 - 影响:匿名投票/提问的"唯一门槛"被自动化绕过;叠加 P0-1 的 IP 伪造,匿名刷票/刷提问几乎无成本。
- 修复:匿名提交改用语服务端校验的 Turnstile/recaptcha;或补
/api/captcha/*限流 + 同 id 尝试 3 次即销毁;对 math 答案做常量时间比较。
P1-4 追问"等主人回复"仅客户端校验,服务端可绕过
- 位置:
route/question/follow_up.go:31-63(PublishFollowUp) - 问题:仅检查
question.Answer != ""与count < 3,不校验上一条追问是否已被回答(waitingFollowUpReply只在模板隐藏表单,POST 不强制)。攻击者可连发满 3 条追问而不等主人回任一条。 - 修复:取该问题最新一条 follow-up,若
Answer == ""直接拒绝(并复用CreateIfBelowLimit原子兜底)。
P1-5 内容审核"可信域名剥离"被绕过,钓鱼链接漏审
- 位置:
internal/security/censor/text.go:89-96(RemoveTrustedURL) - 问题:朴素
strings.ReplaceAll("https://github.com", "")。https://github.com.evil.tld/phish会被剥成.evil.tld/phish,恶意域名逃过 URL 审核。 - 修复:解析 URL 后按"精确 host 或受信 host 的子域"精确匹配,再决定是否跳过审核。
P1-6 密码校验:遗留 HMAC 比较非常量时间 + bcrypt 失败静默降级
- 位置:
internal/db/password.go:56(legacyHash == storedHash)、:24(GenerateFromPassword出错回退弱 HMAC-SHA1) - 问题:遗留账户走
==比较存在时序侧信道;bcrypt 出错时不返回错误,反而存一个弱 HMAC-SHA1 哈希(下次登录按 legacy 处理), silently 削弱了密码存储强度。 - 修复:遗留分支改用
subtle.ConstantTimeCompare;bcrypt 失败直接return err,不回退。建议 bcrypt cost 提到 12。
P1-7 删除内容不回退功能配额计数
- 位置:
internal/db/questions.go(删除)、internal/db/conversation.go:204(MarkDeletedByOwner) - 问题:达定量上限后,即便删除该提问/私聊,对应
feature_controls.quota_count不-1,功能仍显示quota_reached关闭,须箱主手动"重置计数"。 - 修复:删除时按功能类型调用
FeatureControls.IncrementQuota(-1)(注意软删除与并发)。
P1-8 自动封禁把合法 302 误判为探测
- 位置:
internal/route/autoban.go:51-53 - 问题:把"302 → /"一律当扫描信号;但
/i18n/{lang}切换、logout、黑名单跳转、投票不存在等都是 302→/。60s 内 20 次即 403 封 15 分钟,易误伤正常用户。 - 修复:仅在
f.NotFound(真正的 404)路径内计数,排除业务性 302。
P1-9 服务反代路径穿越 + 丢失 query + 无超时
- 位置:
route/service/proxy.go:37,59 - 问题:
forwardPath未经path.Clean,/service/<prefix>/../../x可越过白名单前缀(白名单本身有效,无任意 SSRF/元数据风险);丢失RawQuery;每请求新建Transport且无超时。 - 修复:
Clean后再次校验仍以forwardURL.Path为前缀;透传 query;复用带超时的Transport。
P1-10 /api/v1/pixel/** 豁免 CSRF + 放行写方法 + 无节流 + 响应头透传
- 位置:
internal/context/context.go:170(CSRF 排除)、internal/route/route.go:296(f.Any+reqUserSignIn) - 问题:该前缀跳过 CSRF 校验且
f.Any放行 POST/PUT/DELETE;path.Join("/api/", uri)可经..跳出/api;上游Set-Cookie等响应头原样回传;无速率限制。 - 修复:强制 CSRF 或仅放行 GET;拒绝含
..的路径;白名单放行响应头;补限流。
🟡 P2 — 健壮性与改进(建议项)
| # | 位置 | 问题 | 建议 |
|---|---|---|---|
| P2-1 | route/vote/vote.go:310 |
排序题统计 json.Unmarshal(&data.Ranking,…) 应为 &data,解析必失败且错误被忽略 |
改为 &data 并补单测验证 |
| P2-2 | route/vote/vote.go:48,343,776 |
结果缓存共享指针,且仅新提交失效(删提交/改选项不失效),多实例不一致 | 失效键扩展;或缓存按 vote 维度加版本号 |
| P2-3 | internal/db/vote_submissions.go:51-55 |
唯一索引不含 deleted_at:软删提交后统计归零却无法重投 |
唯一约束纳入 deleted_at 或改用业务去重 |
| P2-4 | internal/db/questions.go |
Question.Token 仅 6 位且无唯一索引约束说明,碰撞可越权看他人私密/未答提问 |
确认是否 uniqueIndex;不足则加长 |
| P2-5 | route/question/question.go |
普通 Delete 不级联 follow_ups/UploadImages,留孤儿记录 |
软删时同步标记子表 |
| P2-6 | internal/db/users.go |
CountTodayMessagesFromUser 用 CURRENT_DATE(DB 时区),与 App 本地时区不符,每日私信上限窗口偏移 |
用 App 本地时区日期边界 |
| P2-7 | route/user/profile.go:337 |
导出 XLSX/CSV 时用户串直写单元格,含 = + - @ 开头可在 Excel 执行公式(公式注入) |
单元格前加 \t/单引号或置文本格式 |
| P2-8 | internal/db/users.go:454 |
注册重复 email/domain 校验存在 TOCTOU,并发同值双过 validate |
包事务或友好去重(唯一索引兜底) |
| P2-9 | internal/route/gzip.go:75-80 |
ct 求值后未用 → 未显式设 Content-Type 的响应永不压缩;204/304 仍加 Content-Encoding |
显式压缩判断 + 跳过无实体状态码 |
| P2-10 | internal/route/route.go:233 |
/i18n/{lang} 仅拦 //,未拦 /\ 反斜杠等价变体 |
归一化 to 后再校验 |
| P2-11 | internal/route/route.go |
缺 HSTS;lang cookie 无 Secure |
加 Strict-Transport-Security;cookie Secure |
| P2-12 | internal/route/chat/chat_handler.go:122 |
SendMessage 未重新校验 FeatureChat.Open(创建时查过,发消息漏查) |
发送前 FeatureStateOf(FeatureChat).Open 校验,或明确设计为既有会话豁免 |
| P2-13 | internal/db/questions.go |
提问列表 PinFirst 与 id<cursor 游标冲突,置顶后"加载更多"会漏/重较新非置顶项 |
置顶与分页采用稳定排序键 |
| P2-14 | internal/captcha/math.go:49 |
忽略 rand.Int 返回错误,极端下 .Int64() 可能 nil 解引用 |
校验错误并 return |
✅ 已核对正确的关键点(信心项)
- 私聊消息计数并发:
IncrementMessageCountIfBelow(conversation.go:124)使用原子条件更新WHERE message_count < max AND is_closed=false,无超额竞态(经核实,非 agent 所报的竞态)。 - 提问归属鉴权:
Questioner中间件 +SetPrivate/Delete/TogglePin/SetRead均以ctx.User.ID==pageUser.ID判定,无 IDOR。 - 投票提交防绕过:
Submit已校验FeatureVote.Open、isVoteActive、已投(HasUserVoted/HasTokenVoted)+ 数据库唯一索引兜底;voter token 为 HMAC 签名。 - 回收密码令牌:64 位随机、24h 有效期、单次使用、绑定
user.ID,设计安全。 - 会话固定:登录后
RegenerateID已做。 - 配额计数:
IncrementQuota原子quota_count + ?+ 并发补加重试,设计合理。 - 启动健壮性:
AutoMigrate前置去重、功能开关 backfill、旧版私聊零值陷阱纠偏,均为幂等且不阻断启动。
修复优先级建议
- 立即(P0):P0-1 反向代理 IP 信任校验。
- 本周(P1):P1-1 会话 Cookie 标志、P1-2 配置解析、P1-3 验证码策略(配合 P0-1)、P1-4 追问服务端校验、P1-5 审核域名绕过、P1-6 密码比较、P1-8 自动封禁误判、P1-10 pixel CSRF。
- 排期(P2):导出公式注入、统计 bug 单测、缓存一致性、时区等。
注:outputs/ 下已有 2026-08-26/08-28 的历史审阅文档,本次为独立复检,新增/重申了 IP 伪造、审核 fail-closed、追问绕过、审核域名绕过、配置崩溃等项;其中 chat 消息计数竞态经核实不存在,已在历史结论中纠正。