diff --git a/SECURITY_TODO.md b/SECURITY_TODO.md index 55d6df8..51a1a99 100644 --- a/SECURITY_TODO.md +++ b/SECURITY_TODO.md @@ -2,6 +2,7 @@ 基于 2026-08-19 的安全审计(源码 + haibara.ai 线上验证)整理。 2026-08-27 复审新增 #18–#25(标题标注"复审新发现")。 +2026-08-27 API 化(/api JSON 接口化,b1e9d9c..76b744d)后复审新增 #26–#32(标题标注"API 化复审新发现")。 按优先级排序,完成后勾选并标注日期。 --- @@ -61,6 +62,17 @@ - [x] 表单 Action URL 由解析后的数值 ID 构造,不再回拼原始路由参数 - **验证**: ✅ `TestAdminUserRoutesRejectNonNumericIDs`(5 组注入串 × GET/POST/DELETE 均被拒、数据零变更、合法 ID 不受影响);变异测试确认旧代码下注入用例失败("1 OR 1=1" 返回 200) +### [x] 26. JSON API 无请求体大小限制——未认证内存耗尽 DoS(2026-08-27 API 化复审新发现)✅ 2026-08-27 +- **位置**: `handlers/api.go`(bindJSON / ShouldBindJSON)、`main.go`(全链路无 `http.MaxBytesReader`) +- **问题**: 全部 POST/PUT/DELETE 已迁入 /api 且以 JSON 提交,但没有任何请求体大小上限:`net/http` 默认不限 body,JSON 解码超长字符串 token 时按需整段分配内存。未认证端点 `/api/auth/login`、`/api/auth/register`、`/api/article/:slug/comments` 均可直接打击——CSRF 不构成障碍(先 GET 登录页/文章页即可取得合法 token 与会话 Cookie)。发送 GB 级 `"username":"AAAA…"` 可使单实例进程 OOM。 +- **说明**: 旧表单端点(ParseForm)同样无限制,属沿袭缺陷而非 API 化引入;但 API 化是补齐的时机。 +- **修复**: + - [x] 新增 `middleware/bodylimit.go` BodyLimit 中间件:非 multipart 请求统一 4 MiB(覆盖文章 Markdown 正文上限);multipart 按平台上传策略派生(启用的类型限制与全局默认取最大值,再 +1 MiB 编码开销)。Content-Length 已知且超限时读体前直接 413;其余经 `http.MaxBytesReader` 封装,读超限即截断 + - [x] `handlers/api.go` bindJSON 识别 `*http.MaxBytesError` 返回 413 + `request_too_large`(i18n 中英新增) + - [x] 中间件顺序调整为 sessions → 会话加固 → SetUserContext → **BodyLimit** → CSRF:必须在 CSRF 之前(CSRF 解析 multipart 会读取整个请求体);SetUserContext 提前使 413 文案可按请求语言翻译 + - [x] 测试环境中间件链同步(security_test.go) +- **验证**: ✅ `TestBodyLimitRejectsOversizedJSON`(已知长度 / chunked 流式两种形态的 4MB+ 登录请求 → 413/request_too_large)、`TestBodyLimitAllowsNormalJSON`(正常体放行至认证层 401)、`TestBodyLimitRejectsOversizedMultipart`(3MB multipart 被拒且附件表零写入)、`TestBodyLimitGETBypass`(GET 不受影响);`go build/vet/test -race ./...` 全绿 + --- ## P1 — 近期修复 @@ -113,6 +125,21 @@ - [x] 附带修复:processAvatar 依赖的 png/gif 解码器此前未注册(旧代码靠"失败回退"掩盖),补 blank import - **验证**: ✅ `TestAddUploadFileTypeRejectsDangerousExtensions`(6 组危险扩展拒绝 + .md 正常)、`TestUploadAvatarRejectsNonImage`(.html 拒绝 / 图片扩展名包 HTML 拒绝 / 正常 PNG 转存 .jpg)、`TestUpdateProfileAvatarRejectsNonImage`(表单头像同样拒绝 + 正常图片成功) +### [ ] 27. /api/auth/register 无速率限制——批量注册垃圾账户(2026-08-27 API 化复审新发现) +- **位置**: `handlers/auth.go`(Register)、`handlers/login_ratelimit.go` +- **问题**: 登录有限流器(#10,IP+用户名),注册完全没有。`allow_registration` 开启时机器人可无限批量注册用户;用户名唯一性检查与 bcrypt cost 12 都不构成成本屏障(注册不触发任何限流计数)。 +- **修复**: + - [ ] 复用 LoginRateLimiter 思路,按 IP 限流注册(建议 10 次/小时/IP,键加前缀区分 login/register),超限 429 + i18n 新增 `register_locked` +- **验证**: [ ] 测试:同 IP 连续注册超阈值 → 429;其他 IP 不受影响;正常注册成功 + +### [ ] 28. 评论提交无速率限制(2026-08-27 API 化复审新发现) +- **位置**: `handlers/comment.go`(PostComment)、`models/seed.go`(默认策略) +- **问题**: 未认证即可提交评论(默认 `AllowGuest=true`),且无任何频率限制;配合默认 `GuestRequireApproval=false`(即时公开显示),开箱即用状态可被灌水机刷屏,同时放大 #26 的攻击面。 +- **修复**: + - [ ] 按 IP(或 IP+文章)限流评论提交(建议 5 条/分钟/IP),超限 429 + i18n 新增 `comments_locked` + - [ ] (可选,产品决策)新部署默认 `GuestRequireApproval=true` +- **验证**: [ ] 测试:同 IP 高频提交 → 429;正常节奏不受影响 + --- ## P2 — 计划修复 @@ -166,6 +193,28 @@ - **修复**: 新增公共 `validateEmail`(空值放行,非空走 `net/mail.ParseAddress`,与评论处口径一致),四处统一调用 - **验证**: ✅ `TestRegisterRejectsInvalidEmail`、`TestProfileEmailValidation`、`TestAdminUserPasswordAndEmailEnforcement`(`abc` 均拒绝、合法邮箱正常) +### [ ] 29. 站点 favicon/logo 上传缺魔数校验(2026-08-27 API 化复审新发现) +- **位置**: `handlers/settings.go`(saveSiteImage) +- **问题**: 头像上传(profile.go)与附件上传(attachment.go)均调用 `contentMatchesType` 做魔数一致性校验(#14/#21),但 saveSiteImage 只查扩展名白名单 + Category=image 即 `io.Copy` 落盘——管理员可把 HTML 内容存为 `logos/logo.png`。当前由 `X-Content-Type-Options: nosniff` + 按扩展名的 Content-Type 兜底(浏览器不会执行),但纵深防御链条在此断裂。 +- **修复**: + - [ ] saveSiteImage 读取字节后调用 `contentMatchesType(check.Type, content)`,不匹配返回 400(与头像上传口径一致) +- **验证**: [ ] 测试:PNG 扩展名 + HTML 字节 → 400;正常 PNG → 200 + +### [ ] 30. 最后管理员防线存在 TOCTOU 竞态(2026-08-27 API 化复审新发现) +- **位置**: `handlers/admin_user.go`(UserUpdate 降级检查、UserDelete 删除检查) +- **问题**: `adminCount <= 1` 检查与后续 Save/Delete 非原子:两个并发的"降级/删除最后一位管理员"请求可同时通过检查,导致站点失去管理员。SQLite 单写锁下窗口极小;MySQL 部署是真实窗口(需管理员 CSRF 或双开标签配合,可利用性低)。 +- **修复**: + - [ ] 检查+写入包进 `db.Transaction`,事务内先计数再更新(MySQL 下依赖行锁或 `SELECT ... FOR UPDATE`) +- **验证**: [ ] 测试:并发降级最后管理员的请求,最终至少保留一个 admin(`-race`) + +### [ ] 31. 普通作者可置顶全站文章——需确认设计意图(2026-08-27 API 化复审新发现) +- **位置**: `handlers/article.go`(ArticleCreate/ArticleUpdate 由 /api/my/articles 复用)、`templates/user/my_article_form.html`(is_top 复选框) +- **问题**: my 表单与 API 均接受 `is_top`——任意注册作者可把自己的文章钉在全站首页最顶端(`publishedArticleOrder` 为 is_top DESC 优先),还能自定 `published_at` 影响排序。若"作者可置顶自己的文章"非产品预期,属影响公共展示位的横向越权。 +- **修复**(若确认非设计意图): + - [ ] MyArticleCreate / MyArticleUpdate 强制 `is_top=false`(仅 admin 路径接受该字段) + - [ ] my_article_form.html 移除置顶复选框 +- **验证**: [ ] 测试:作者经 /api/my/articles 提交 is_top=true → 落库为 false;admin 路径不受影响 + --- ## P3 — 低优先级 / 观察项 @@ -198,6 +247,14 @@ - [x] 用户不存在时也执行一次 dummy bcrypt 比较(包级预生成哑哈希),抹平时间差;两分支均记录失败计数 - **验证**: ✅ 结构保证两分支均执行一次 bcrypt(`TestLoginTimingDoesNotRevealUser` 断言未知用户分支进入 Fail);大样本计时统计属人工运维验证,逻辑上两分支 B 树一致 +### [ ] 32. 零碎加固(2026-08-27 API 化复审新发现) +- **位置**: 多处 +- **问题与修复**: + - [ ] `admin_user.go` UserUpdate:`status` 无枚举校验,可存任意 int(如 99)——限定 {0,1,2,3},非法 400 + - [ ] `attachment.go` parseUintParam / parseUintForm:`Sscanf("%d")` 会把 `"5abc"` 宽松解析为 5——改 `strconv.ParseUint` 严格拒绝(无注入风险,值已为数值类型,仅严谨性) + - [ ] `settings.go` dangerousUploadExtensions:补充 `.xsl` / `.xslt` / `.shtml`(nosniff 已兜底,仅完整性) +- **验证**: [ ] 表驱动测试:非法 status → 400;`"5abc"` 形式的 id → 拒绝 + --- ## 不需要修复(已确认安全,2026-08-27 复审复核仍成立) @@ -208,12 +265,25 @@ - 附件路径穿越:SHA-256 内容寻址文件名 - 线上默认凭据:已修改(已验证) - 注册接口:已关闭(已验证) +- 开放重定向:API 响应的 redirect 字段均为服务端常量(APIOK 只接收 handler 硬编码路径),用户输入不进入跳转目标 +- JS 上下文注入:文章正文 `{{.Article.Content}}` 处于 `