# 安全评估与客户端 IP 信任 报告:`outputs/security-audit-2026-10-25.md`;复现脚本 `outputs/sec-audit-2026-10-25/`(可重跑)。 ## 客户端 IP 采信(P1-3,2026-10-26 已修) - 唯一读取点:`internal/shared/ip.go` 的 `ResolveClientIP`,23 处调用共用。 **不要在别处另读 `CF-Connecting-IP` / `EO-Client-IP`**,否则绕过校验。 - 规则:权威头只在 `headerSourceTrusted(remote)` 为真时采信 —— 对端是回环地址,或命中 `trusted_proxies`。公网对端带来的权威头一律忽略,回退 remote。 - XFF / X-Real-IP 仍只认 `trusted_proxies`;回环**不**隐含信任这两个头。 - 公网对端却带权威头 → 进程内一条 WARN(`remote_addr` + `header_ip`)。 出现这条日志 = 要么有人在伪造,要么 CDN 直连回源但没登记网段。 - 不能靠「哪个头存在」推断请求走了哪家 CDN:客户端自带任意头都能反向利用。 ## 部署形态 - 生产/测试都是 `trust_proxy = true`,回源 = **cloudflared Tunnel 回本机**(对端 127.0.0.1), 因此修复后零配置变更。 - **EdgeOne 已取消**(境内访问速度不理想),当前只有 Cloudflare 一条线。 重新启用前必须把 EO 回源网段写进 `[security] trusted_proxies`,否则 EO 线退化成按边缘 IP 聚合 (限流互挤、自动封禁误伤整片)。注释已写进 `conf/app.sample.ini`。 - `box.shiroko.one` CNAME 到第三方 `cloudflare.182682.xyz`(CF SaaS 优选),该 zone 的 Transform/WAF 不归我们管 —— 别把安全前提建立在那边的规则上。 - 源站隐藏:外网尽力排查(A 记录 / certspotter 51 个子域 / MX / 候选 IP)未找到直连入口; 仍须服务器侧 `dev/security-selfcheck.sh local` 确认 `listen = 127.0.0.1:5877`。 ## 实测确认「防护到位」(别重复怀疑) CSRF 绑 uid(无 token POST → 400);18 条 XSS 载荷全灭(含实体编码、`java\tscript:`、 协议相对 `//`、反斜杠);跨箱题目/投票越权全 302;页面密码对主页/整箱分享图/单题分享图/API 四个出口都生效;pixel/service/ShareCard 无 SSRF;`exec cwebp` 不接用户输入;SMTP 默认校验证书; `RegenerateID` + `pwd_ver`;TLS1.0/1.1 拒绝;无 CORS 头;测试站蜜罐已开。 ## 已修(2026-10-26) - **P1-1** 外链 ``:`markdown.go` 的 `safeImageSrc` 只放行站内相对路径与自有图片 CDN 主机 (配置来源 `image_bucket_cdn_host` / `aliyun_bucket_cdn_host` / `.` / `external_url`); `a[href]` 仍允许外站。无合法 src 的 img 整段丢弃。 - **P1-2** 密码:page/ask 密码 `minlen:6`(`form.PasswordMinLen`)+ handler 二次校验; 提问密码比对挪到验证码之后(`askPasswordDenied`,私聊与普通提问各调一次); 页面密码失败锁定 `shared.AttemptLimiter(5, 10min, 10min)`,键 = 箱主ID + LimitKey(IP)。 - **P1-3** 权威头:见上「客户端 IP 采信」。 - 部署后验收:`python outputs/sec-audit-2026-10-25/verify_p1_fixes.py`,四项应全 PASS (旧版本上实测四项全 FAIL)。跑完会自动还原严格度/密码/测试提问。 ## 第二轮修复(2026-10-27,P2/P3 全清) - **跳转白名单收敛为唯一实现 `shared.SafeRedirectTarget`**(`internal/shared/redirect.go`), 登录 `to` 与 `/_//password` 的 `next` 共用。规则:拒绝空/含 `\`、控制字符、空格; 必须单斜杠开头、非 `//`;`url.Parse` 后不得有 Scheme/Host/User/Opaque;遍历 `u.Path` 的 rune 拒 `\`、`<0x20`、`0x7f`(**关键**:`url.Parse` 会把 `%0d%0a` 解码成 CRLF,只看原始串抓不住); 最后 `path.Clean` 再验一次前缀。测试在 `redirect_test.go`(25 条定向 + 全量不变式)。 - **注册防枚举**:邮箱重复/已停用与全新注册走**同一个** `registerSucceeded(ctx)` 出口 (302 → `/login`、无 alert),日志里才留痕。域名占用仍明文提示(不然用户没法选)。 代价:忘密用户只能走「找回密码」。 - **CSP**:`internal/route/csp.go` + `[security] csp = off|report|enforce`(默认 report)、 `csp_report_uri`。`script-src` 白名单必须含 **`https://mami.shiroko.one`**—— `templates/base/header.html` 用 `document.createElement("script")` 动态注 umami, 静态 grep `