Files
tamabox/.workbuddy/memory/2026-10-26.md
T
tamakyi 430c6ef6d8 docs(memory): 记录审计结论(CSP 默认 enforce、分享卡字体限制、复查新发现)
- topics/sec.md:两轮修复的结论、全局复查新发现(私聊 POST 越权、页面密码 next)、
  CSP 白名单口径、字体只能用 glyf 轮廓这条硬限制,以及复现脚本的两种「假红」
- MEMORY.md:安全小节补跳转白名单、GET/POST 校验不对称、CSP/umami、注册防枚举
- 2026-10-27.md / topics/build.md:本地 WSL 部署与验收的具体跑法
2026-10-27 12:37:55 +08:00

4.5 KiB
Raw Blame History

2026-10-26

修复 P1-3:CDN 权威客户端头未校验来源

用户确认:生产/测试 trust_proxy = true,回源是 cloudflared Tunnel 回本机(对端 127.0.0.1); 境内 EdgeOne 已取消(实测期国内速度不理想),将来可能重新启用。

改动

  • internal/shared/ip.go
    • 新增 headerSourceTrusted(remote):对端是回环 或 命中 trusted_proxies 才允许采信权威头。
    • ResolveClientIP 把 CF-Connecting-IP / EO-Client-IP 读取整体收进该判断;公网对端忽略。
    • 公网对端带权威头 → sync.Once 一条 WARN(remote_addr + header_ip),覆盖「伪造」与 「CDN 直连回源忘登记网段」两种情形。
    • XFF / X-Real-IP 信任范围未变(仍只认 trusted_proxies,回环不隐含信任)。
  • internal/shared/ip_test.go:原 4 个用例按新契约调整;新增 RejectsForgedHeaderFromPublicPeer(4 种伪造组合)TrustedProxyHonorsCDNHeader IPv6LoopbackHeader LoopbackDoesNotTrustXFF;加 setTrustedProxies 助手(还原包级状态)。
  • conf/app.sample.ini(CRLF,用脚本按行块改,未破坏行尾)+ internal/conf/static.go 注释同步。

验证

  • 变异验证:把 headerSourceTrusted(remote) 临时改成 true(旧逻辑)→ 回归用例 3 个子项失败; 还原后全绿。确认测试咬得住。
  • go build -overlay dev/overlay.json ./... 通过;排除 internal/storage(overlay 桩缺 fitsWithin/aspect 等辅助函数,属既有问题)后全包测试通过。

环境坑(值得记)

  • 本机 go 是 1.25.5,go.mod 要 1.27.3;GOSUMDB=off 会让工具链校验失败( "verifying module: checksum database disabled by GOSUMDB=off"),GOTOOLCHAIN=local 又降级到 1.25.5。 解法:直接调缓存里的工具链 ~/go/pkg/mod/golang.org/[email protected]/bin/go.exe,配 GOTOOLCHAIN=local。
  • Python 读 CRLF 文件要显式 newline='',否则 universal newline 把 \r\n 归一成 \n, split('\r\n') 只会得到 1 行(踩过一次,误判文件被写坏)。

待办

P1-1(img src 白名单)、P1-2(密码 minlen + 验证码顺序)、P2-3(to 开放重定向)均未动。

修复 P1-1 / P1-2

P1-1 外链图片(internal/template/markdown.go)

  • 新增 safeImageSrc():<img src> 只允许站内相对路径(无协议部分)或主机命中自有图片出口 (image_bucket_cdn_host / aliyun_bucket_cdn_host / <bucket>.<endpoint> / external_url 主机)。
  • 含反斜杠一律拒(\ 在浏览器等价 /);// 先于「以 / 开头=站内」判断; url.Parse + Hostname() 比对 → 后缀 / userinfo 把戏失效。
  • img 无合法 src 时整段丢弃(只删 src 会留 <img alt> 变成破图)。
  • a[href] 仍走 safeURL(外链需点击,产品上要允许)。
  • 测试:TestSafeImageSrc(12 条拒绝)+ TestMarkdownToHTML_ImageSource(端到端); 变异验证:safeImageSrc 直接 return true → 7 条拒绝用例失败。

P1-2 密码(5 个文件)

  • minlen:6 + form.PasswordMinLen;handler 里 rejectTooShortPassword() 二次校验(rune 计数); 前端 minlength=6 maxlength=30 + 提示文案「6–30 位,留空则关闭」;i18n 新增 form.profile.passwordLen / question.errPasswordLocked(4 语言)。
  • 校验顺序:抽 askPasswordDenied(),从 New() 首行挪到两个分支验证码之后(私聊 + 普通提问各一次)。 没把验证码提到最前——会让「箱已关闭」「需登录」变成验证码错误。
  • 失败锁定:新增 shared.AttemptLimiter(无后台 goroutine,惰性清理 + 键数上限); pagePasswordGuard(5, 10min, 10min),键 = 箱主ID + LimitKey(IP)。锁定期内不比对密码。

端到端验收脚本

outputs/sec-audit-2026-10-25/verify_p1_fixes.py(--only p1-1,p1-2a,p1-2b,p1-2c)。 在未部署的旧版本上四项全 FAIL,已实测 → 证明检测有效;部署后应全 PASS。

踩到的两个判定坑(写脚本时记下来):

  • 提问成功页是 200(不是 302),不能用状态码判断提交成功。
  • 受保护的箱是在 /_/<domain> 内联渲染密码表单(200),不是跳转 /password; 判断「有没有密码闸门」只能看页面里有没有 action="/_/<domain>/password"。
  • 箱主页与 /api/v1/user/<domain>/questions 都不暴露题目 id(无后台), 要定位刚建的提问只能扫 /_/<domain>/<id>(脚本里 question_ids_with_marker)。