fix: 完成 SECURITY_TODO #27–#32 剩余安全修复(限流/魔数校验/最后管理员事务化/置顶收紧/零碎加固) #4

Merged
kevin merged 6 commits from dsh/go_blog:fix/security-todo-27-32 into main 2026-08-27 23:22:15 +08:00
Contributor

对应 SECURITY_TODO.md 剩余的 6 项(#27–#32,2026-08-27 复审发现),每项一个 commit:

  • #27(P1)注册接口限流:新增 handlers/rate_limit.go WindowRateLimiter(固定窗口、有界 map、与 #10 同款淘汰策略);Register 按 IP 限流 10 次/小时,超限 429 + i18n register_locked
  • #28(P1)评论提交限流PostComment 按 IP 限流 5 条/分钟,超限 429 + i18n comments_locked(复用 #27 限流器)
  • #29(P2)favicon/logo 魔数校验saveSiteImage 与头像/附件上传(#14/#21)口径一致,读取字节后 contentMatchesType 校验,不匹配 400(settings_upload_bad_content
  • #30(P2)最后管理员 TOCTOU:检查+写入包进 db.Transaction;MySQL 下 SELECT ... FOR UPDATEclause.Locking)串行化并发事务;SQLite 无 FOR UPDATE,叠加进程内互斥锁 lastAdminMu(单实例部署);顺带修复f.Role 覆盖 user.Role 后检查恒真的旧隐患
  • #31(P2)作者置顶收紧:确认"作者可置顶"非设计意图;作者 create 强制 is_top=false、update 保留库中现有值(管理员授权置顶不因作者编辑丢失);作者表单移除置顶复选框;admin 路径不受影响
  • #32(P3)零碎加固UserCreate/UserUpdate status 枚举校验 {0,1,2,3}(非法 400);parseUintParam/parseUintFormstrconv.ParseUint 严格解析("5abc" 不再宽松为 5);危险扩展名补充 .xsl/.xslt/.shtml

验证

  • go build ./...go vet ./... 通过
  • go test -race ./... 全绿,新增测试:
    • TestWindowLimiterFixedWindow / TestRegisterRateLimited / TestCommentRateLimited
    • TestSiteImageUploadRejectsMismatchedContent
    • TestConcurrentLastAdminDowngrade
    • TestMyArticlesCannotPin
    • TestUserStatusEnumRejected / TestParseUintStrict / TestAddUploadFileTypeRejectsDangerousExtensions(扩展用例)

说明

  • #28 可选项"新部署默认 GuestRequireApproval=true"为产品决策,未随本项实施,留待确认
  • #31 原标注"需确认产品意图",本 PR 按描述中的收紧方案实施;若产品预期作者可自行置顶,请告知以回退
对应 `SECURITY_TODO.md` 剩余的 6 项(#27–#32,2026-08-27 复审发现),每项一个 commit: - **#27(P1)注册接口限流**:新增 `handlers/rate_limit.go` `WindowRateLimiter`(固定窗口、有界 map、与 #10 同款淘汰策略);`Register` 按 IP 限流 10 次/小时,超限 429 + i18n `register_locked` - **#28(P1)评论提交限流**:`PostComment` 按 IP 限流 5 条/分钟,超限 429 + i18n `comments_locked`(复用 #27 限流器) - **#29(P2)favicon/logo 魔数校验**:`saveSiteImage` 与头像/附件上传(#14/#21)口径一致,读取字节后 `contentMatchesType` 校验,不匹配 400(`settings_upload_bad_content`) - **#30(P2)最后管理员 TOCTOU**:检查+写入包进 `db.Transaction`;MySQL 下 `SELECT ... FOR UPDATE`(`clause.Locking`)串行化并发事务;SQLite 无 FOR UPDATE,叠加进程内互斥锁 `lastAdminMu`(单实例部署);顺带修复`f.Role` 覆盖 `user.Role` 后检查恒真的旧隐患 - **#31(P2)作者置顶收紧**:确认"作者可置顶"非设计意图;作者 create 强制 `is_top=false`、update 保留库中现有值(管理员授权置顶不因作者编辑丢失);作者表单移除置顶复选框;admin 路径不受影响 - **#32(P3)零碎加固**:`UserCreate/UserUpdate` status 枚举校验 {0,1,2,3}(非法 400);`parseUintParam/parseUintForm` 改 `strconv.ParseUint` 严格解析("5abc" 不再宽松为 5);危险扩展名补充 `.xsl/.xslt/.shtml` ## 验证 - `go build ./...`、`go vet ./...` 通过 - `go test -race ./...` 全绿,新增测试: - `TestWindowLimiterFixedWindow` / `TestRegisterRateLimited` / `TestCommentRateLimited` - `TestSiteImageUploadRejectsMismatchedContent` - `TestConcurrentLastAdminDowngrade` - `TestMyArticlesCannotPin` - `TestUserStatusEnumRejected` / `TestParseUintStrict` / `TestAddUploadFileTypeRejectsDangerousExtensions`(扩展用例) ## 说明 - #28 可选项"新部署默认 `GuestRequireApproval=true`"为产品决策,未随本项实施,留待确认 - #31 原标注"需确认产品意图",本 PR 按描述中的收紧方案实施;若产品预期作者可自行置顶,请告知以回退
dsh added 6 commits 2026-08-27 21:56:37 +08:00
register/api 未认证端点可被无限批量注册:bcrypt cost 12 与用户名唯一
检查不构成成本屏障。新增 WindowRateLimiter(固定窗口、有界 map、与
#10 相同的淘汰策略),Register 按 IP 限流 10 次/小时,超限 429 +
i18n register_locked(中英)。

- handlers/rate_limit.go: 通用固定窗口限流器(可注入时钟便于测试)
- handlers/auth.go: Register(db, limiter),键前缀与登录限流器区分
- main.go: 注册限流器接线;测试环境同步
- 测试: TestWindowLimiterFixedWindow / TestRegisterRateLimited
  (同 IP 10 次成功、第 11 次 429、其他 IP 不受影响)

注: #28 评论限流将复用同一限流器,在下一个 commit 实施。
评论端点未认证即可提交(默认 AllowGuest=true 且即时公开),配合默认
GuestRequireApproval=false 开箱即用状态可被灌水机刷屏。PostComment
复用 #27 的 WindowRateLimiter 按 IP 限流 5 条/分钟,超限 429 +
i18n comments_locked(中英)。

- handlers/comment.go: PostComment(db, limiter),键前缀区分
- main.go / security_test.go: 评论限流器接线
- 测试: TestCommentRateLimited(同 IP 5 次成功、第 6 次 429、
  其他 IP 不受影响)

注: 可选项“新部署默认 GuestRequireApproval=true”为产品决策,未随本项实施。
saveSiteImage 只查扩展名白名单 + Category=image 即落盘,管理员可将
HTML 字节存为 logos/logo.png(当前仅靠 nosniff + 按扩展名 Content-Type
兜底,纵深防御链条断裂)。现与头像/附件上传(#14/#21)口径一致:
读取字节后 contentMatchesType 校验,不匹配返回 400
(settings_upload_bad_content,i18n 中英新增)。

- handlers/settings.go: saveSiteImage 魔数校验后再写盘
- 测试: TestSiteImageUploadRejectsMismatchedContent
  (favicon/logo 各:PNG 扩展名 + HTML 字节 → 400;真实 PNG → 200)
UserUpdate/UserDelete 的 adminCount<=1 计数与 Save/Delete 非原子,两个
并发的'降级/删除倒数第二位管理员'请求可同时通过检查,站点失去管理员
(MySQL 下真实窗口;SQLite 单写锁下窗口极小)。

- handlers/admin_user.go: 检查+写入包进 db.Transaction;
  ensureNotLastAdmin 在事务内锁定管理员集合后再计数:
  MySQL 用 SELECT ... FOR UPDATE(gorm clause.Locking)串行化
  并发事务;SQLite 无 FOR UPDATE,叠加进程内互斥锁 lastAdminMu
  (应用按设计单实例部署),计数在写锁串行化后重读
- 顺带修正:原实现中 f.Role 覆盖 user.Role 后检查恒为 true 的语义
  保留(基于原角色判定),避免降级检查被新值掩盖
- 测试: TestConcurrentLastAdminDowngrade(两位管理员并发降级:
  恰好 1 成功 1 拒绝、最终至少保留一位管理员;-race 通过)
任意注册作者可经 /api/my/articles 提交 is_top=true 把文章钉在全站
首页最顶端(publishedArticleOrder 为 is_top DESC 优先),属影响公共
展示位的横向越权(确认非设计意图,予收紧)。

- handlers/article.go: 拆出共享实现 articleCreate/articleUpdate,
  增加 allowIsTop 开关;admin 路径保持 true
- handlers/my_articles.go: 作者创建强制 is_top=false;编辑保留库中
  现有值(管理员授权的置顶不因作者编辑而丢失,作者也无法自行取消)
- templates/user/my_article_form.html: 移除置顶复选框
- 测试: TestMyArticlesCannotPin(作者 create/update is_top=true →
  落库 false;admin 路径可置顶;作者编辑不丢置顶)
- admin_user.go: UserCreate/UserUpdate 的 status 无枚举校验,可存任意
  int(如 99)——新增 validUserStatus 限定 {0,1,2,3},非法 400
  (i18n user_status_invalid 中英新增)
- attachment.go: parseUintParam/parseUintForm 由 fmt.Sscanf("%d") 改为
  strconv.ParseUint 严格解析——"5abc" 不再被宽松解析为 5
- settings.go: dangerousUploadExtensions 补充 .xsl/.xslt/.shtml
  (XSLT 可内嵌脚本、SSI 可包含文件;nosniff 已兜底,仅完整性)
- 测试: TestUserStatusEnumRejected / TestParseUintStrict /
  TestAddUploadFileTypeRejectsDangerousExtensions 扩展用例
kevin merged commit 9bdb2d8556 into main 2026-08-27 23:22:15 +08:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: kevin/go_blog#4