- topics/sec.md:两轮修复的结论、全局复查新发现(私聊 POST 越权、页面密码 next)、 CSP 白名单口径、字体只能用 glyf 轮廓这条硬限制,以及复现脚本的两种「假红」 - MEMORY.md:安全小节补跳转白名单、GET/POST 校验不对称、CSP/umami、注册防枚举 - 2026-10-27.md / topics/build.md:本地 WSL 部署与验收的具体跑法
8.8 KiB
2026-10-25
/stats 文案清理:把「写给运营者看」的话删掉
用户指出「数据速览」下面那句「下面几句是从上面的数字直接读出来的,没有推测」怪 —— 那是对他自己说的(作者视角的免责),不是对访客说的。顺着扫了页面全部 40 余条文案。
改动:
- 删
stats.insight.note(四语言 + 模板那行<p class="tb-card-note">)。 卡片留「数据速览」+ 列表即可;stats.insight.empty保留。 删掉说明行后标题与列表之间只剩 4px,给.tb-insights补margin: 8px 0 0。 stats.foot.cacheNote去掉实现细节:「当天重算一次的快照(当天第一次访问时算, 服务重启也会重算)」→「累计数字与图表每天更新一次」。访客不需要知道缓存是懒重算、 也不需要知道重启会触发重算;准确的新鲜度由「快照时间」那行给出。- 顺手修了
templates/stats.html里一条过期注释(还写着「四条时间」「基准日」, 上一轮已改成三行「快照时间」)。
保留未动(判断过,不属于这个问题):
stats.foot.privacy与顶部stats.lead的后半句近乎同义(隐私声明重复一次)—— 页脚隐私声明是惯例,重复不算噪音,故保留。stats.today.tag的「(实时)」在 60s TTL 下仍成立,不改。
约定(已写进 topics/stats.md)
页面文案一律以访客为第二人称对象:不写实现细节(缓存、重启、索引),
不写作者视角的免责(「没有推测」「这里只有」「本文」这类把读者当审稿人的话)。
数据来源/隐私/新鲜度这些确实要对访客交代的事,放 stats.lead 与页脚注。
环比句改成「点明月份 + 条数」
用户问「上月提问量环比 -75.0%」是不是「上月同期比本月同期」。不是——实际比的是 两个完整自然月(9 月 vs 8 月)。但这个问题本身就是证据:句子里「上月」和「环比」 两个相对词叠着,读者得先做两次翻译,很容易读成「同期对比」。
- 新句:
9 月收到 5,620 条提问,比 8 月的 4,890 条多了 14.9%。EN:September had 5,620 questions; August had 4,890, up 14.9%. - 口径没变(仍是上月 vs 上上月,本月没过完不参与),只换表达。
page.go新增monthText:CJK 一律「9 月」,英文用time.Month().String()("month 9" 不是英语),不带年份——相比的两个月相邻。- 三个词条
momUp/momDown/momFlat取代mom:方向词没法塞进同一个模板, 四种语言语序不同。参数顺序统一(本月份名/条数、对比月名/条数、|pct|)。 |pct| < 0.05走持平,避免「多了 0.0%」。- 测试:
TestInsightMoMNamesBothMonths(上升/下降/持平三分支,断言两个月份名 + 两个条数都在句子里)、TestMonthText。 - 顺手修预览测试:
GrowthMoM原来写死 12.3,与monthly造的数据对不上, 而新句会同时印出两个月的条数 → 截图会显示「5620 比 4890 多了 12.3%」这种假证据。 改成从monthly[10]/monthly[9]算。
安全评估(AI 挖洞背景下的一次全量自检)
报告:outputs/security-audit-2026-10-25.md,脚本在 outputs/sec-audit-2026-10-25/。
结论:无 getshell / SQLi / 未授权读数据;但 2 个 P1 直击「匿名」承诺。
P1-1 匿名提问可植入远程图片(去匿名化)
markdown.go的safeURL只校验协议,<img src=https://任意域>能原样输出。- 唯一挡住它的是审核「广告_引流类」维度,而该维度只在 strict 档生效
(
censor/policy.go:standard 只有 Abuse/Contraband,lax 为空)。 - 实测:账号切 standard →
<img src="https://tb871870.example.org/standard.png">直接入库并渲染; 切回 strict 才被拒。→ 修法必须在markdown.go层限制 img src 为站内/白名单,不能依赖档位。 - 同时确认 8 种混淆(实体编码、
java\tscript:、协议相对//、反斜杠)都被审核识别。
P1-2 访问密码可爆破 + 提问密码校验早于验证码
UpdatePagePassword/UpdateAskPassword只有maxlen:30,无 minlen,可设 1 位。page.go的New()第一件事就比对 AskPassword,验证码要到后面才校验 → 实测「错误提问密码 +deadbeef:999999」返回「提问密码错误」而不是验证码错误 → 猜密码完全不消耗验证码。限额 30/min(ask)、10/min(page)。
其余P2/P3
- 登录开放重定向:
to=/\evil.example→Location: /\evil.example,浏览器按 WHATWG 把\当/→https://evil.example/。path.Clean不处理反斜杠。 - 注册接口枚举邮箱(返回「已注册」),与 forgot-password 的防枚举口径不一致。
- 无 CSP;
X-XSS-Protection: 1过时;/user无Cache-Control: no-store。 - 限流/自动封禁是进程内存态(多副本叠加、重启清零)。
- 匿名投票身份 =
vote_fpcookie,清 cookie 即换身份,仅 200/min/IP 兜底。 - 登录
to兜底是死代码:path.Clean("/"+to)恒非空 → 登录后永远落/而非箱主页。 - JSON 响应无
Content-Type(实测 text/plain);429 无Retry-After。
无法从外网确认的一项
trust_proxy 真实值。三种伪造头(XFF / X-Real-IP / EO-Client-IP)各 22 次都不被采信
(好的方向:无法伪造绕限流),但这既可能是 trust_proxy=false(→ 按 CF 边缘 IP 聚合,
限流与封禁会误伤整片区域),也可能是 true 且在 CF 侧删了 EO-Client-IP。需登服务器看 app.ini。
已验证没事(别重复怀疑)
CSRF 绑 uid(无 token POST → 400);18 条 XSS 载荷全灭;跨箱题目/投票越权全 302;
页面密码对主页/整箱分享图/单题分享图/API 四个出口都生效;pixel/service/ShareCard 无 SSRF;
exec cwebp 不接用户输入;SMTP 默认校验证书;RegenerateID + pwd_ver;TLS1.0/1.1 拒绝;
无 CORS 头;测试站蜜罐已开(/.git/HEAD、/.env 返回假后台)。
测试卫生
- 测试站账号
tbunread1022181904,脚本已把严格度恢复 strict、清空 page/ask 密码、 删除本轮 15 条测试提问(115–129)。下次复用前先确认这三项是干净的。 - 隔离 venv 里装了
requests(~/.workbuddy/binaries/python/envs/default), 比 urllib 顺手;/api/captcha/generate限流 20/min,批量提交脚本必须带 429 重试。
安全评估(续):trust_proxy=true 引出的 P1-3
用户确认两套环境 trust_proxy = true。据此重做了 IP 采信那一块,发现比原来的推断严重:
P1-3 双 CDN 下权威头优先次序反了
internal/shared/ip.go的ResolveClientIP无条件先读CF-Connecting-IP,再读EO-Client-IP。- 前提「权威头只能由 CDN 写入」只对 Cloudflare 成立:
- CF:强制覆盖
CF-Connecting-IP,客户端伪造直接 403(生产/测试站实测一致)。 - EdgeOne:不剥离客户端传入的
CF-Connecting-IP,share.shiroko.one实测带该头返回 200 (响应头Server: edgeone makers/EO-LOG-UUID)。
- CF:强制覆盖
- 代码优先读 CF 那个头 → 走 EO 线路时客户端 IP 完全可控。后果:限流/IP 黑名单全失效,
且
autoban.Record可被用来栽赃任意 IP(填别人的 IP 连打 10 次敏感路径 → 对方被封)。 - 今天打不通:
box.shiroko.one境内递归(AliDNS DoH)只解析到cloudflare.182682.xyz→ 104.18.x/172.64.x, 文档里写的境内eo.dnse4.com线路已不在;box.tama.guru301 跳新域名也走 CF。差一条 DNS 记录。 - 修法:权威头也套
isTrustedProxy(remote)(回环可用 cloudflared 本机场景),fail-closed; EO 侧删CF-Connecting-IP;日志同时记remote_addr与解析后 IP。
源站定位(外网已尽力,未找到)
box.shiroko.oneCNAMEcloudflare.182682.xyz(第三方 CF SaaS 优选,zone 不在我们手里, 其 Transform/WAF 规则无法自查)。- certspotter 枚举
*.shiroko.one11 个、*.337845818.best19 个子域,逐个解析全部落 Cloudflare / 腾讯 EdgeOne / Vercel(216.198.79.x、64.29.17.x),无一条直连记录。 status.shiroko.one、share.shiroko.one→ 43.174.246.43/247.43,用 box 的 SNI 访问回 418 Teapot → 是 EO 边缘不是源站。- 结论:源站隐藏目前成功,但「找不到」≠「不可达」,仍需服务器侧
security-selfcheck.sh local确认listen = 127.0.0.1:5877。
顺带修正
上一版报告曾把「三种伪造头都不生效」推断为 trust_proxy 可能为 false —— 那是错的解读:
无论 true/false,限流键都落在我自己的真实 IP 上,外部不可分辨。已在报告第四节改正。