Files
go_blog/SECURITY_TODO.md
T
dsh 52ca450ddd fix: SECURITY_TODO #28 评论提交按 IP 限流(5 条/分钟)防灌水刷屏
评论端点未认证即可提交(默认 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”为产品决策,未随本项实施。
2026-08-27 21:39:28 +08:00

27 KiB
Raw Blame History

安全修复 TODO

基于 2026-08-19 的安全审计(源码 + haibara.ai 线上验证)整理。 2026-08-27 复审新增 #18#25(标题标注"复审新发现")。 2026-08-27 API 化(/api JSON 接口化,b1e9d9c..76b744d)后复审新增 #26#32(标题标注"API 化复审新发现")。 按优先级排序,完成后勾选并标注日期。


P0 — 立即修复

[x] 1. 会话密钥弱回退(可伪造管理员会话) 2026-08-19

  • 位置: config/config.gogenerateSecret / applyDefaults 回退)
  • 问题: secret 缺失时回退为 SHA-256(主机名+PID),两者均可被外部推测/爆破,攻击者可离线伪造任意用户会话 cookie。
  • 修复:
    • generateSecret() 改用 crypto/rand 生成 32 字节随机数
    • 已有配置加载路径中 secret 为空时:拒绝启动(log.Fatalf),不再静默回退;配置文件读取失败也改为直接退出
    • 首次生成配置文件时写入强随机 secret(保持 install_linux.sh 的 openssl 路径不变)
  • 验证: 新密钥为 crypto/rand 输出;缺失 secret 时启动直接报错

[x] 2. 全站无 CSRF 防护 2026-08-19

  • 位置: 全部 POST 路由(登录/注册/文章/评论/管理后台/设置/附件)
  • 问题: 仅靠 cookie 认证,无 CSRF token;线上 cookie 无 SameSite 属性,浏览器默认 Lax 保护不完整(Chrome Lax+POST 豁免、Safari 差异)。
  • 修复:
    • 新增 middleware/csrf.go:同步器令牌模式(session 存储、常量时间比较),表单 _csrf 字段或 X-CSRF-Token 头二选一,不匹配返回 403
    • 覆盖全部 30 个 POST 表单(含游客评论表单);AJAX(附件上传/删除、头像上传)经 <meta name="csrf-token"> 下发 token 并以请求头携带
    • /article/:slug/comments 游客 POST 一并覆盖(游客同样有 session)
  • 验证: middleware/csrf_test.go 7 用例 + 端到端 curl 冒烟(无 token/伪造 token 403,有效 token 302

[x] 3. 附件接口越权(IDOR 2026-08-19

  • 位置: handlers/attachment.goDeleteAttachment / ListAttachments / UploadAttachment 的 article_id
  • 问题: /my/articles/attachments/* 仅要求登录,无所有权校验;任意登录用户可删除/列出全站任意附件、向他人文章挂附件。
  • 修复:
    • DeleteAttachment:admin / 上传者 / 所属文章作者三者之一,否则 403
    • ListAttachments:文章作者或 admin,否则 403
    • UploadAttachmentarticle_id != 0 时校验文章归属(admin 除外),否则 403
    • 单元测试(handlers/security_test.go:越权 403 / 本人 200 / admin 覆盖)
  • 验证: 普通用户 A 删除用户 B 的附件 -> 403(测试覆盖)

[x] 4. 会话固定(Session Fixation 2026-08-19

  • 位置: handlers/auth.goLogin / Register 自动登录)
  • 问题: 登录成功后未清空旧 session,直接写入 user_id,固定攻击可劫持登录后会话。
  • 修复:
    • 认证成功后先 session.Clear() 再写入 user_id/username 并 Save;保留 lang 与 csrf_token(避免多标签页已渲染表单失效)
  • 验证: 登录前后 cookie 值不同,旧 cookie 无法访问受保护路由(TestLoginRotatesSession

[x] 18. SQLite 数据库文件可被公开下载(全库泄露)(2026-08-27 复审新发现) 2026-08-27

  • 位置: main.go:72router.Static("/uploads", cfg.Path) 把存储根整体挂载为静态目录)、models/db.go:36blog.db 就放在该目录下)
  • 问题: 未认证即可 GET /uploads/blog.db 下载整库——含 bcrypt 密码哈希、用户邮箱、评论者 IP/邮箱、私密评论(IsPrivate)、浏览记录等。
  • 修复(采用方案 B,白名单挂载):
    • main.go 新增 registerUploadRoutes:只挂载 attachments/avatars/logos/ 及当前配置的附件存储目录,存储根不再整体暴露
    • 自定义 serveUploadDir handler:禁用目录列表(目录/../\ 一律 404),只服务具体文件
    • safeStorageDirstorage_dir 含穿越/绝对路径/反斜杠时回退 attachments(为 #22 提供纵深防御)
    • (可选加固)方案 A:将数据库文件移出存储根,需迁移存量 blog.db,暂缓
  • 验证: main_test.go 4 用例(blog.db 404 / 无目录列表 / 穿越失败 / 三目录正常服务、自定义 storage_dir、不安全回退、重名去重)

[x] 19. GORM 字符串条件 SQL 注入(admin 用户管理 3 处)(2026-08-27 复审新发现) 2026-08-27

  • 位置: handlers/admin_user.go:253、289、362db.First(&user, id)id 为 c.Param("id") 字符串)
  • 问题: GORM 对 First(dest, s):s 为非数字字符串且无附加参数时按原始 SQL 条件拼入 WHEREv1.31.1 statement.go:293-303 已核实)。GET /admin/users/1 OR 1=1/edit 即可注入;UserEditPage 为 GET 无 CSRF 拦截,可诱导已登录管理员点击链接触发盲注。
  • 修复:
    • UserEditPage / UserUpdate / UserDelete 三处:先 uintFormID 解析为数值,非数值(0)直接 302 回列表页;查询一律走主键参数化
    • 表单 Action URL 由解析后的数值 ID 构造,不再回拼原始路由参数
  • 验证: TestAdminUserRoutesRejectNonNumericIDs5 组注入串 × GET/POST/DELETE 均被拒、数据零变更、合法 ID 不受影响);变异测试确认旧代码下注入用例失败("1 OR 1=1" 返回 200

[x] 26. JSON API 无请求体大小限制——未认证内存耗尽 DoS(2026-08-27 API 化复审新发现) 2026-08-27

  • 位置: handlers/api.gobindJSON / ShouldBindJSON)、main.go(全链路无 http.MaxBytesReader
  • 问题: 全部 POST/PUT/DELETE 已迁入 /api 且以 JSON 提交,但没有任何请求体大小上限:net/http 默认不限 bodyJSON 解码超长字符串 token 时按需整段分配内存。未认证端点 /api/auth/login/api/auth/register/api/article/:slug/comments 均可直接打击——CSRF 不构成障碍(先 GET 登录页/文章页即可取得合法 token 与会话 Cookie)。发送 GB 级 "username":"AAAA…" 可使单实例进程 OOM。
  • 说明: 旧表单端点(ParseForm)同样无限制,属沿袭缺陷而非 API 化引入;但 API 化是补齐的时机。
  • 修复:
    • 新增 middleware/bodylimit.go BodyLimit 中间件:非 multipart 请求统一 4 MiB(覆盖文章 Markdown 正文上限);multipart 按平台上传策略派生(启用的类型限制与全局默认取最大值,再 +1 MiB 编码开销)。Content-Length 已知且超限时读体前直接 413;其余经 http.MaxBytesReader 封装,读超限即截断
    • handlers/api.go bindJSON 识别 *http.MaxBytesError 返回 413 + request_too_largei18n 中英新增)
    • 中间件顺序调整为 sessions → 会话加固 → SetUserContext → BodyLimit → CSRF:必须在 CSRF 之前(CSRF 解析 multipart 会读取整个请求体);SetUserContext 提前使 413 文案可按请求语言翻译
    • 测试环境中间件链同步(security_test.go
  • 验证: TestBodyLimitRejectsOversizedJSON(已知长度 / chunked 流式两种形态的 4MB+ 登录请求 → 413/request_too_large)、TestBodyLimitAllowsNormalJSON(正常体放行至认证层 401)、TestBodyLimitRejectsOversizedMultipart3MB multipart 被拒且附件表零写入)、TestBodyLimitGETBypassGET 不受影响);go build/vet/test -race ./... 全绿

P1 — 近期修复

  • 位置: main.gosession store)、handlers/comment.go:82comment_uid
  • 修复:
    • store 默认 SameSite: LaxSecure 按请求动态设置(middleware/https.go 检测 TLS 或 X-Forwarded-Proto),通过中间件在每次请求时应用到 session cookie
    • comment_uid 游客 cookie 同步补齐 SameSite=Lax + HTTPS 下 Secure
  • 验证: 模拟 HTTPS 请求响应头 Set-Cookie: ... HttpOnly; Secure; SameSite=Lax;冒烟测试通过

[x] 6. 缺失安全响应头 2026-08-19

  • 位置: 新增 middleware/security_headers.go(全局第一个注册)
  • 修复:
    • Content-Security-Policydefault-src 'self' + 现有 CDN 白名单 + frame-ancestors 'none' 等)
    • X-Content-Type-Options: nosniffX-Frame-Options: DENYReferrer-PolicyPermissions-Policy
    • Strict-Transport-Security(仅 HTTPS 请求下发,未加 includeSubDomains 以免影响 HTTP 子域)
  • 说明: CSP 含 'unsafe-inline'(模板内联 script/style 必需);待 P2-9 CDN 本地化后可进一步收紧
  • 验证: middleware/security_headers_test.goheaders 存在性、HSTS 条件下发)+ 冒烟 curl 确认

[x] 7. X-Forwarded-For 伪造(IP 审计/浏览量可刷) 2026-08-19

  • 位置: handlers/helpers.goGetClientIP)、config/config.goWebConfig.TrustedProxies)、main.go
  • 修复:
    • 删除手动解析 XFF 首值逻辑,GetClientIP 改为 c.ClientIP()
    • router.SetTrustedProxies(cfg.Web.TrustedProxies);新增 web.trusted_proxies 配置项(默认 ["127.0.0.1", "::1"]unix socket 部署自动信任)
    • gin 内部 XFF 从右往左取第一个不可信 IP:直接客户端伪造的 XFF 被忽略
  • 验证: middleware/clientip_test.go(直接连接带假 XFF 取真实 IP / 代理链取最右不可信条目)

[x] 8. goroutine 数据竞争(use-after-return 2026-08-19

  • 位置: handlers/home.goArticleDetail → recordArticleView
  • 修复:
    • goroutine 启动前同步提取 userID / ip / UA 为局部变量,recordArticleView 不再触碰 gin.Context 与 session
  • 验证: go test -race ./... 全绿

[x] 20. 被禁用/锁定/删除用户的会话不失效(2026-08-27 复审新发现) 2026-08-27

  • 位置: middleware/auth.go:16-27AuthRequired 只看 session 是否有 user_id,不回库校验)
  • 问题: 登录时的状态检查(handlers/auth.go:50)只在登录瞬间生效。管理员禁用/锁定/软删用户后,其已持有的 cookie 在最长 24h 内仍完全可用:发评论自动 Approved、写文章、传附件;SetUserContext 对已软删用户仍置 is_logged_in=true
  • 修复:
    • AuthRequired(db) 回库校验 Status == StatusNormal 且未软删,失败则清 session(保留 lang 与 csrf_token)并跳转 /loginsession user_id 一律先断言为数值再入 GORM(呼应 #19)
    • SetUserContext:用户查询失败或非正常状态时 is_logged_in 置 false(评论自动通过随之失效,回落游客审核策略)
  • 验证: TestDisabledUserSessionInvalidateddisabled/locked 旧 cookie → 302 /login)、TestSoftDeletedUserSessionInvalidatedTestDisabledUserCommentsRequireApproval(锁定后评论转 pending

[x] 21. 头像上传缺类别校验 + 可添加任意扩展名 → 存储型 XSS 链(2026-08-27 复审新发现) 2026-08-27

  • 位置: handlers/profile.go:196-208UploadAvatar 未限制 image 类别)、handlers/profile.go:223-226processAvatar 失败回退存原始字节)、handlers/profile.go:96-107UpdateProfile 头像分支同样无类别校验、原样落盘)、handlers/settings.go:269-290addUploadFileType 无危险扩展黑名单)
  • 问题: logo/favicon 上传要求 Category == imagesettings.go:113/153),但头像上传只查扩展名白名单且解码失败仍存原始文件;管理员又可在上传设置里添加任意扩展名(含 .html/.svg)。组合链:添加 .html 类型 → 任意登录用户以头像名义上传 HTML → 落在 /uploads/avatars/ 同源可执行(CSP script-src 'self' 'unsafe-inline' 放行)。
  • 修复:
    • UploadAvatar / UpdateProfile 头像分支强制 check.Type.Category == models.CategoryImage,解码失败直接拒绝(不再回退存原始字节)
    • UpdateProfile 头像同样经 processAvatar 解码→缩放→JPEG 重编码,原始字节不再落盘
    • addUploadFileType 增加危险扩展黑名单(.html/.htm/.xhtml/.xht/.svg/.xml/.js/.mjs),拒绝添加并提示(settings_upload 页新增错误提示 + i18n
    • 附带修复:processAvatar 依赖的 png/gif 解码器此前未注册(旧代码靠"失败回退"掩盖),补 blank import
  • 验证: TestAddUploadFileTypeRejectsDangerousExtensions(6 组危险扩展拒绝 + .md 正常)、TestUploadAvatarRejectsNonImage(.html 拒绝 / 图片扩展名包 HTML 拒绝 / 正常 PNG 转存 .jpg)、TestUpdateProfileAvatarRejectsNonImage(表单头像同样拒绝 + 正常图片成功)

[x] 27. /api/auth/register 无速率限制——批量注册垃圾账户(2026-08-27 API 化复审新发现) 2026-08-27

  • 位置: handlers/auth.goRegister)、handlers/login_ratelimit.go
  • 问题: 登录有限流器(#10,IP+用户名),注册完全没有。allow_registration 开启时机器人可无限批量注册用户;用户名唯一性检查与 bcrypt cost 12 都不构成成本屏障(注册不触发任何限流计数)。
  • 修复:
    • 新增 handlers/rate_limit.go WindowRateLimiter:固定窗口计数限流器(与 #10 相同的有界 map + 淘汰策略,键前缀区分 login/register
    • Register 按 IP 限流注册(10 次/小时/IP),超限 429 + i18n 新增 register_locked(中英)
  • 验证: TestRegisterRateLimited(同 IP 连续 10 次成功、第 11 次 429/register_locked、其他 IP 不受影响)、TestWindowLimiterFixedWindow(窗口内超限拒绝 / 窗口翻转重置 / 键隔离)

[x] 28. 评论提交无速率限制(2026-08-27 API 化复审新发现) 2026-08-27

  • 位置: handlers/comment.goPostComment)、models/seed.go(默认策略)
  • 问题: 未认证即可提交评论(默认 AllowGuest=true),且无任何频率限制;配合默认 GuestRequireApproval=false(即时公开显示),开箱即用状态可被灌水机刷屏,同时放大 #26 的攻击面。
  • 修复:
    • PostComment 按 IP 限流评论提交(5 条/分钟/IP,复用 #27 的 WindowRateLimiter),超限 429 + i18n 新增 comments_locked(中英)
    • (可选,产品决策)新部署默认 GuestRequireApproval=true——留待产品确认,未随本项实施
  • 验证: TestCommentRateLimited(同 IP 5 次成功、第 6 次 429/comments_locked、其他 IP 不受影响)

P2 — 计划修复

[x] 9. 第三方 CDN 无 SRI / Tailwind dev CDN 2026-08-27

  • 位置: templates/layouts/base.html:16-20:118-122middleware/security_headers.go
  • 修复:
    • marked / DOMPurify / highlight.js / cropperjs / easymde 固定版本下载至 static/vendor/,经 go:embed 本地分发(easymde 自含 CodeMirror;拼写检查字典为可选外链,断网静默降级)
    • cdn.tailwindcss.com 替换为构建期静态 CSSscripts/build_tailwind.shtailwindcss 3.4.17content 扫 templates+handlers+main.go 保证 Go 侧拼接类不漏),产物 static/css/app.css 提交仓库
    • 配合 #6 收紧 CSP 为 script-src 'self' 'unsafe-inline',移除全部 CDN 域名
  • 验证: 断网第三方域名后页面渲染功能完整(puppeteer 冒烟:首页/编辑器资源 200、Tailwind 样式生效、无 JS 报错);CSP 已无第三方来源

[x] 10. 登录无速率限制 2026-08-27

  • 位置: handlers/auth.go:33
  • 修复:
    • 新建 handlers/login_ratelimit.go:内存限速器(IP+用户名 key),5 次失败锁 15 分钟,成功登录清零,map 有界(4096 上限 + 惰性/最老淘汰)
    • 锁定期间返回 ?error=locked 明确提示(不泄露用户存在性);失败提示保持统一 ?error=1
  • 验证: TestLoginRateLimited(5 次失败→锁定→正确密码也被拒→Reset 恢复→其他用户不受影响)

[x] 11. 配置文件权限过宽 2026-08-27

  • 位置: config/config.go
  • 修复: os.WriteFile(configFile, data, 0644)0640(secret 写入后不再组/世界可读;install_linux.sh 原有 0640 保持一致)
  • 验证: TestConfigFileCreatedNotWorldReadable(创建后 perm == 0640

[x] 12. 首启弱凭据 admin/admin 2026-08-27(方案 A

  • 位置: models/db.go
  • 修复(方案 A:
    • 首启生成 16 位随机密码(crypto/rand,字母表排除易混淆字符),一次性打印日志;不再使用 admin/admin
  • 说明: 线上已改密(已验证),此项为防御新部署
  • 验证: TestRandomAdminPassword(长度/字符合法/两次生成不同)

[x] 13. Unix socket 权限 666 2026-08-27

  • 位置: install_linux.sh:80
  • 修复: chmod 666chown blog_go:blog_go + chmod 660,安装结束打印提示:反向代理运行用户需 usermod -aG blog_go <proxy_user>
  • 说明: 部署脚本改动,需在 Linux 环境验证(本机无法执行);本机任意用户已不能再绕过 Cloudflare 直连

[x] 22. storage_dir 路径穿越 2026-08-27

  • 位置: handlers/settings.gosaveUploadConfig)、handlers/attachment.gomain.go safeStorageDir
  • 修复:
    • saveUploadConfig 校验存储目录为单个安全路径段(^[A-Za-z0-9_-]+$,手写 safeStorageDirName),非法直接拒绝并提示 ?error=illegal_diri18n 新增)
    • 保留 main.go safeStorageDir 运行时兜底作为纵深防御(不改)
  • 验证: TestStorageDirTraversalRejected(6 组穿越值均拒绝且库中值不变 / 合法值正常保存)+ TestSafeStorageDirNameAndValidators 纯函数表驱动

[x] 23. 密码策略缺失(改密/管理员建号无最小长度) 2026-08-27

  • 位置: handlers/profile.go(改密)、handlers/admin_user.go UserCreate/UserUpdate
  • 修复: 新增公共 validatePassword(≥6 位,与注册口径一致),三处统一调用,失败回渲染表单/跳转 + i18n 提示(profile_password_short / user_password_short
  • 验证: TestProfilePasswordMinLength(1 位拒绝且旧哈希保留 / 6 位成功)、TestAdminUserPasswordAndEmailEnforcement(建号/重置短密码均拒绝)

[x] 24. 邮箱字段不校验格式 2026-08-27

  • 位置: handlers/auth.go(注册)、handlers/profile.go(改邮箱)、handlers/admin_user.go(建号/编辑)
  • 修复: 新增公共 validateEmail(空值放行,非空走 net/mail.ParseAddress,与评论处口径一致),四处统一调用
  • 验证: TestRegisterRejectsInvalidEmailTestProfileEmailValidationTestAdminUserPasswordAndEmailEnforcementabc 均拒绝、合法邮箱正常)

[ ] 29. 站点 favicon/logo 上传缺魔数校验(2026-08-27 API 化复审新发现)

  • 位置: handlers/settings.gosaveSiteImage
  • 问题: 头像上传(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.goUserUpdate 降级检查、UserDelete 删除检查)
  • 问题: adminCount <= 1 检查与后续 Save/Delete 非原子:两个并发的"降级/删除最后一位管理员"请求可同时通过检查,导致站点失去管理员。SQLite 单写锁下窗口极小;MySQL 部署是真实窗口(需管理员 CSRF 或双开标签配合,可利用性低)。
  • 修复:
    • 检查+写入包进 db.Transaction,事务内先计数再更新(MySQL 下依赖行锁或 SELECT ... FOR UPDATE
  • 验证: [ ] 测试:并发降级最后管理员的请求,最终至少保留一个 admin(-race

[ ] 31. 普通作者可置顶全站文章——需确认设计意图(2026-08-27 API 化复审新发现)

  • 位置: handlers/article.goArticleCreate/ArticleUpdate 由 /api/my/articles 复用)、templates/user/my_article_form.htmlis_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 → 落库为 falseadmin 路径不受影响

P3 — 低优先级 / 观察项

[x] 14. 上传不校验文件真实类型 2026-08-27

  • 位置: handlers/upload_validator.go(新增 contentMatchesType)、handlers/attachment.gohandlers/profile.go
  • 修复: mimetypev1.4.12 转直接依赖)magic-bytes 检测与扩展名配置的 MimeType 比对(宽容策略:策略为空/application/octet-stream/内容不可检测时放行,扩展名白名单仍为主闸);附件 AJAX 400 + 头像 400/表单错误
  • 验证: TestUploadAttachmentRejectsMismatchedContent.txt 内容为 PNG 字节 → 400,真文本 → 200)、TestContentMatchesTypeTable8 用例表驱动)

[x] 15. Gravatar MD5 邮箱哈希可反查 2026-08-27

  • 位置: models/seed.gomodels/comment_config.godefaultCommentConfig)、handlers/admin_comment.gotemplates/admin/comment_list.html
  • 修复: 新部署默认 UseGravatar=falsegorm tag default:false 同步);前端注释占位(AuthorInitial + 调色板)已是既有模式;管理员评论列表跟随开关,关闭时不再请求 gravatar.com;管理员可在评论设置页显式重开
  • 说明: 线上已存在配置行不受 default 迁移影响,后台关闭即可;协议固有反查风险保留(开启者知情)
  • 验证: TestGravatarOffByDefaultTestAdminCommentListFollowsGravatarSwitch(关:无 gravatar.com;开:出现)

[x] 16. RSS 以 Host 头构造 baseURL 2026-08-27

  • 位置: models/site_setting.go(新增 SiteURL)、handlers/settings.gotemplates/admin/settings_site.htmlhandlers/rss.go
  • 修复: 站点设置新增规范地址(SiteURL,保存时 trim);RSSFeed 优先使用固定 URL(去尾斜杠),未配置时告警日志 + 回退请求 Host(兼容旧部署)
  • 验证: TestRSSUsesConfiguredSiteURL(配置后 Host 头污染不生效 / 未配置回退)

[x] 17. bcrypt cost 偏低 2026-08-27

  • 位置: models/user.goSetPassword)、handlers/login_ratelimit.godummyHash
  • 修复: bcrypt.DefaultCost(10) → 12models 包常量 bcryptCost,dummy 哈希同 cost);已有哈希自适应不失效,下次改密自然升级
  • 验证: 现有认证/限速测试全绿(含 -race);成本升级使登录延迟 ~300ms,配合 #10 限速可接受

[x] 25. 登录计时侧信道(用户名枚举) 2026-08-27(与 #10 一并实施)

  • 位置: handlers/auth.go:39-47
  • 问题: 用户不存在时立即返回、不执行 bcrypt;密码错误时执行 bcrypt(~100ms)。响应时间差可用于枚举有效用户名,与未修复的 #10(无速率限制)叠加放大。
  • 修复:
    • 用户不存在时也执行一次 dummy bcrypt 比较(包级预生成哑哈希),抹平时间差;两分支均记录失败计数
  • 验证: 结构保证两分支均执行一次 bcrypt(TestLoginTimingDoesNotRevealUser 断言未知用户分支进入 Fail);大样本计时统计属人工运维验证,逻辑上两分支 B 树一致

[ ] 32. 零碎加固(2026-08-27 API 化复审新发现)

  • 位置: 多处
  • 问题与修复:
    • admin_user.go UserUpdatestatus 无枚举校验,可存任意 int(如 99)——限定 {0,1,2,3},非法 400
    • attachment.go parseUintParam / parseUintFormSscanf("%d") 会把 "5abc" 宽松解析为 5——改 strconv.ParseUint 严格拒绝(无注入风险,值已为数值类型,仅严谨性)
    • settings.go dangerousUploadExtensions:补充 .xsl / .xslt / .shtmlnosniff 已兜底,仅完整性)
  • 验证: [ ] 表驱动测试:非法 status → 400;"5abc" 形式的 id → 拒绝

不需要修复(已确认安全,2026-08-27 复审复核仍成立)

  • SQL 注入:全参数化查询(GORM)——唯一例外见 #19admin_user.go 3 处字符串条件)
  • XSShtml/template 自动转义 + 评论双防御(服务端 strip + DOMPurify
  • 密码哈希:bcrypt
  • 附件路径穿越:SHA-256 内容寻址文件名
  • 线上默认凭据:已修改(已验证)
  • 注册接口:已关闭(已验证)
  • 开放重定向:API 响应的 redirect 字段均为服务端常量(APIOK 只接收 handler 硬编码路径),用户输入不进入跳转目标
  • JS 上下文注入:文章正文 {{.Article.Content}} 处于 <script> 字符串上下文,html/template 自动做 JS 转义;评论另经 data-md 属性 + DOMPurify 渲染
  • API 越权(IDOR):/api/my/* 全部带 author_id 所有权约束;附件三端点均有 admin/上传者/文章作者校验;admin 与 my 的文章 CRUD 分组挂不同中间件
  • API 化后旧表单端点已全部移除:不存在新旧端点权限口径不一致的并行暴露面

建议执行顺序

#1–#25 及 #26 已修复并验证。剩余待办 #27–#32,建议顺序:

  1. #26 请求体大小限制 2026-08-27
  2. #27/#28 注册与评论限流(P1,可与 #26 的中间件基建衔接实施)
  3. #29 favicon/logo 魔数校验#30 最后管理员事务化P2,各自独立小改)
  4. #31 置顶权限需先确认产品意图(作者可置顶是否预期)再决定修否
  5. #32 零碎项随手修

历史遗留观察项(不阻塞):

  • #18 方案 A:数据库文件移出存储根,需迁移存量 blog.db
  • #15 Gravatar 开启时的反查风险(管理员知情)
  • #9 本地化 vendor 库版本升级提醒(随浏览器生态更新,重建 scripts/build_tailwind.sh 与 vendor 文件)