19 KiB
19 KiB
安全修复 TODO
基于 2026-08-19 的安全审计(源码 + haibara.ai 线上验证)整理。 2026-08-27 复审新增 #18–#25(标题标注"复审新发现")。 按优先级排序,完成后勾选并标注日期。
P0 — 立即修复
[x] 1. 会话密钥弱回退(可伪造管理员会话)✅ 2026-08-19
- 位置:
config/config.go(generateSecret/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.go7 用例 + 端到端 curl 冒烟(无 token/伪造 token 403,有效 token 302)
[x] 3. 附件接口越权(IDOR)✅ 2026-08-19
- 位置:
handlers/attachment.go(DeleteAttachment / ListAttachments / UploadAttachment 的 article_id) - 问题:
/my/articles/attachments/*仅要求登录,无所有权校验;任意登录用户可删除/列出全站任意附件、向他人文章挂附件。 - 修复:
DeleteAttachment:admin / 上传者 / 所属文章作者三者之一,否则 403ListAttachments:文章作者或 admin,否则 403UploadAttachment:article_id != 0时校验文章归属(admin 除外),否则 403- 单元测试(
handlers/security_test.go:越权 403 / 本人 200 / admin 覆盖)
- 验证: ✅ 普通用户 A 删除用户 B 的附件 -> 403(测试覆盖)
[x] 4. 会话固定(Session Fixation)✅ 2026-08-19
- 位置:
handlers/auth.go(Login / 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:72(router.Static("/uploads", cfg.Path)把存储根整体挂载为静态目录)、models/db.go:36(blog.db就放在该目录下) - 问题: 未认证即可
GET /uploads/blog.db下载整库——含 bcrypt 密码哈希、用户邮箱、评论者 IP/邮箱、私密评论(IsPrivate)、浏览记录等。 - 修复(采用方案 B,白名单挂载):
main.go新增registerUploadRoutes:只挂载attachments/、avatars/、logos/及当前配置的附件存储目录,存储根不再整体暴露- 自定义
serveUploadDirhandler:禁用目录列表(目录/../\一律 404),只服务具体文件 safeStorageDir:storage_dir 含穿越/绝对路径/反斜杠时回退attachments(为 #22 提供纵深防御)- (可选加固)方案 A:将数据库文件移出存储根,需迁移存量 blog.db,暂缓
- 验证: ✅
main_test.go4 用例(blog.db 404 / 无目录列表 / 穿越失败 / 三目录正常服务、自定义 storage_dir、不安全回退、重名去重)
[x] 19. GORM 字符串条件 SQL 注入(admin 用户管理 3 处)(2026-08-27 复审新发现)✅ 2026-08-27
- 位置:
handlers/admin_user.go:253、289、362(db.First(&user, id),id 为c.Param("id")字符串) - 问题: GORM 对
First(dest, s):s 为非数字字符串且无附加参数时按原始 SQL 条件拼入 WHERE(v1.31.1statement.go:293-303已核实)。GET /admin/users/1 OR 1=1/edit即可注入;UserEditPage 为 GET 无 CSRF 拦截,可诱导已登录管理员点击链接触发盲注。 - 修复:
- UserEditPage / UserUpdate / UserDelete 三处:先
uintFormID解析为数值,非数值(0)直接 302 回列表页;查询一律走主键参数化 - 表单 Action URL 由解析后的数值 ID 构造,不再回拼原始路由参数
- UserEditPage / UserUpdate / UserDelete 三处:先
- 验证: ✅
TestAdminUserRoutesRejectNonNumericIDs(5 组注入串 × GET/POST/DELETE 均被拒、数据零变更、合法 ID 不受影响);变异测试确认旧代码下注入用例失败("1 OR 1=1" 返回 200)
P1 — 近期修复
[x] 5. Cookie 缺 Secure / SameSite 标志 ✅ 2026-08-19
- 位置:
main.go(session store)、handlers/comment.go:82(comment_uid) - 修复:
- store 默认
SameSite: Lax;Secure按请求动态设置(middleware/https.go检测 TLS 或 X-Forwarded-Proto),通过中间件在每次请求时应用到 session cookie comment_uid游客 cookie 同步补齐SameSite=Lax+ HTTPS 下Secure
- store 默认
- 验证: ✅ 模拟 HTTPS 请求响应头
Set-Cookie: ... HttpOnly; Secure; SameSite=Lax;冒烟测试通过
[x] 6. 缺失安全响应头 ✅ 2026-08-19
- 位置: 新增
middleware/security_headers.go(全局第一个注册) - 修复:
Content-Security-Policy(default-src 'self' + 现有 CDN 白名单 + frame-ancestors 'none' 等)X-Content-Type-Options: nosniff、X-Frame-Options: DENY、Referrer-Policy、Permissions-PolicyStrict-Transport-Security(仅 HTTPS 请求下发,未加 includeSubDomains 以免影响 HTTP 子域)
- 说明: CSP 含
'unsafe-inline'(模板内联 script/style 必需);待 P2-9 CDN 本地化后可进一步收紧 - 验证: ✅
middleware/security_headers_test.go(headers 存在性、HSTS 条件下发)+ 冒烟 curl 确认
[x] 7. X-Forwarded-For 伪造(IP 审计/浏览量可刷)✅ 2026-08-19
- 位置:
handlers/helpers.go(GetClientIP)、config/config.go(WebConfig.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 被忽略
- 删除手动解析 XFF 首值逻辑,
- 验证: ✅
middleware/clientip_test.go(直接连接带假 XFF 取真实 IP / 代理链取最右不可信条目)
[x] 8. goroutine 数据竞争(use-after-return)✅ 2026-08-19
- 位置:
handlers/home.go(ArticleDetail → recordArticleView) - 修复:
- goroutine 启动前同步提取 userID / ip / UA 为局部变量,
recordArticleView不再触碰 gin.Context 与 session
- goroutine 启动前同步提取 userID / ip / UA 为局部变量,
- 验证: ✅
go test -race ./...全绿
[x] 20. 被禁用/锁定/删除用户的会话不失效(2026-08-27 复审新发现)✅ 2026-08-27
- 位置:
middleware/auth.go:16-27(AuthRequired 只看 session 是否有 user_id,不回库校验) - 问题: 登录时的状态检查(
handlers/auth.go:50)只在登录瞬间生效。管理员禁用/锁定/软删用户后,其已持有的 cookie 在最长 24h 内仍完全可用:发评论自动 Approved、写文章、传附件;SetUserContext对已软删用户仍置is_logged_in=true。 - 修复:
AuthRequired(db)回库校验Status == StatusNormal且未软删,失败则清 session(保留 lang 与 csrf_token)并跳转 /login;session user_id 一律先断言为数值再入 GORM(呼应 #19)- SetUserContext:用户查询失败或非正常状态时
is_logged_in置 false(评论自动通过随之失效,回落游客审核策略)
- 验证: ✅
TestDisabledUserSessionInvalidated(disabled/locked 旧 cookie → 302 /login)、TestSoftDeletedUserSessionInvalidated、TestDisabledUserCommentsRequireApproval(锁定后评论转 pending)
[x] 21. 头像上传缺类别校验 + 可添加任意扩展名 → 存储型 XSS 链(2026-08-27 复审新发现)✅ 2026-08-27
- 位置:
handlers/profile.go:196-208(UploadAvatar 未限制 image 类别)、handlers/profile.go:223-226(processAvatar 失败回退存原始字节)、handlers/profile.go:96-107(UpdateProfile 头像分支同样无类别校验、原样落盘)、handlers/settings.go:269-290(addUploadFileType 无危险扩展黑名单) - 问题: logo/favicon 上传要求
Category == image(settings.go:113/153),但头像上传只查扩展名白名单且解码失败仍存原始文件;管理员又可在上传设置里添加任意扩展名(含.html/.svg)。组合链:添加.html类型 → 任意登录用户以头像名义上传 HTML → 落在/uploads/avatars/同源可执行(CSPscript-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
- UploadAvatar / UpdateProfile 头像分支强制
- 验证: ✅
TestAddUploadFileTypeRejectsDangerousExtensions(6 组危险扩展拒绝 + .md 正常)、TestUploadAvatarRejectsNonImage(.html 拒绝 / 图片扩展名包 HTML 拒绝 / 正常 PNG 转存 .jpg)、TestUpdateProfileAvatarRejectsNonImage(表单头像同样拒绝 + 正常图片成功)
P2 — 计划修复
[x] 9. 第三方 CDN 无 SRI / Tailwind dev CDN ✅ 2026-08-27
- 位置:
templates/layouts/base.html:16-20、:118-122、middleware/security_headers.go - 修复:
- marked / DOMPurify / highlight.js / cropperjs / easymde 固定版本下载至
static/vendor/,经 go:embed 本地分发(easymde 自含 CodeMirror;拼写检查字典为可选外链,断网静默降级) cdn.tailwindcss.com替换为构建期静态 CSS:scripts/build_tailwind.sh(tailwindcss 3.4.17,content 扫 templates+handlers+main.go 保证 Go 侧拼接类不漏),产物static/css/app.css提交仓库- 配合 #6 收紧 CSP 为
script-src 'self' 'unsafe-inline',移除全部 CDN 域名
- marked / DOMPurify / highlight.js / cropperjs / easymde 固定版本下载至
- 验证: ✅ 断网第三方域名后页面渲染功能完整(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 666→chown blog_go:blog_go + chmod 660,安装结束打印提示:反向代理运行用户需usermod -aG blog_go <proxy_user> - 说明: 部署脚本改动,需在 Linux 环境验证(本机无法执行);本机任意用户已不能再绕过 Cloudflare 直连
[x] 22. storage_dir 路径穿越 ✅ 2026-08-27
- 位置:
handlers/settings.go(saveUploadConfig)、handlers/attachment.go、main.gosafeStorageDir - 修复:
- saveUploadConfig 校验存储目录为单个安全路径段(
^[A-Za-z0-9_-]+$,手写 safeStorageDirName),非法直接拒绝并提示?error=illegal_dir(i18n 新增) - 保留 main.go
safeStorageDir运行时兜底作为纵深防御(不改)
- saveUploadConfig 校验存储目录为单个安全路径段(
- 验证: ✅
TestStorageDirTraversalRejected(6 组穿越值均拒绝且库中值不变 / 合法值正常保存)+TestSafeStorageDirNameAndValidators纯函数表驱动
[x] 23. 密码策略缺失(改密/管理员建号无最小长度)✅ 2026-08-27
- 位置:
handlers/profile.go(改密)、handlers/admin_user.goUserCreate/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,与评论处口径一致),四处统一调用 - 验证: ✅
TestRegisterRejectsInvalidEmail、TestProfileEmailValidation、TestAdminUserPasswordAndEmailEnforcement(abc均拒绝、合法邮箱正常)
P3 — 低优先级 / 观察项
[x] 14. 上传不校验文件真实类型 ✅ 2026-08-27
- 位置:
handlers/upload_validator.go(新增contentMatchesType)、handlers/attachment.go、handlers/profile.go - 修复:
mimetype(v1.4.12 转直接依赖)magic-bytes 检测与扩展名配置的 MimeType 比对(宽容策略:策略为空/application/octet-stream/内容不可检测时放行,扩展名白名单仍为主闸);附件 AJAX 400 + 头像 400/表单错误 - 验证: ✅
TestUploadAttachmentRejectsMismatchedContent(.txt 内容为 PNG 字节 → 400,真文本 → 200)、TestContentMatchesTypeTable(8 用例表驱动)
[x] 15. Gravatar MD5 邮箱哈希可反查 ✅ 2026-08-27
- 位置:
models/seed.go、models/comment_config.go(defaultCommentConfig)、handlers/admin_comment.go、templates/admin/comment_list.html - 修复: 新部署默认
UseGravatar=false(gorm tag default:false 同步);前端注释占位(AuthorInitial + 调色板)已是既有模式;管理员评论列表跟随开关,关闭时不再请求 gravatar.com;管理员可在评论设置页显式重开 - 说明: 线上已存在配置行不受 default 迁移影响,后台关闭即可;协议固有反查风险保留(开启者知情)
- 验证: ✅
TestGravatarOffByDefault、TestAdminCommentListFollowsGravatarSwitch(关:无 gravatar.com;开:出现)
[x] 16. RSS 以 Host 头构造 baseURL ✅ 2026-08-27
- 位置:
models/site_setting.go(新增 SiteURL)、handlers/settings.go、templates/admin/settings_site.html、handlers/rss.go - 修复: 站点设置新增规范地址(SiteURL,保存时 trim);RSSFeed 优先使用固定 URL(去尾斜杠),未配置时告警日志 + 回退请求 Host(兼容旧部署)
- 验证: ✅
TestRSSUsesConfiguredSiteURL(配置后 Host 头污染不生效 / 未配置回退)
[x] 17. bcrypt cost 偏低 ✅ 2026-08-27
- 位置:
models/user.go(SetPassword)、handlers/login_ratelimit.go(dummyHash) - 修复:
bcrypt.DefaultCost(10) → 12(models 包常量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 树一致
不需要修复(已确认安全,2026-08-27 复审复核仍成立)
- SQL 注入:全参数化查询(GORM)——唯一例外见 #19(admin_user.go 3 处字符串条件)
- XSS:html/template 自动转义 + 评论双防御(服务端 strip + DOMPurify)
- 密码哈希:bcrypt
- 附件路径穿越:SHA-256 内容寻址文件名
- 线上默认凭据:已修改(已验证)
- 注册接口:已关闭(已验证)
建议执行顺序
全部 25 项(含 P3-14/15/16/17、P3-25)均已修复并验证,仅 #18 方案 A(数据库移出存储根)为可选项:
- (可选)#18 方案 A:数据库文件移出存储根,需迁移存量 blog.db
- 持续观察项:#15 Gravatar 开启时的反查风险(管理员知情)、#9 本地化 vendor 库版本升级提醒(随浏览器生态更新,重建
scripts/build_tailwind.sh与 vendor 文件)