feat(autoban): 封禁响应图改为 ban_image_path 配置,默认仍是纯文字
上一版把 403 图 //go:embed 进了二进制。实际部署里图会集中放在 R2 统一管理、 由运维分发到各机器,进仓库反而成了负担:换一张图就要重新编译。 改为配置驱动: - 新增 [security] ban_image_path(相对路径以进程工作目录为基准); - 留空 / 文件读不到 / 是目录 / 不是图片 / 超过 2 MB → 一律回退为纯文字 "403 Forbidden",即改造前的老行为。封禁响应是最后一道兜底,不能因为 一张图缺失就变成 500,更不能把站点拦在启动阶段(失败只 Warn); - 启动时读一次装进 atomic.Pointer,热路径零磁盘 IO;换图 = 换文件 + 重启。 安全细节:按内容嗅探(http.DetectContentType)而不是扩展名,必须是 image/*。 把 ban_image_path 配成 app.ini / .env 会被拒绝并告警 —— 否则等于把配置文件 原样吐给每一个被封的攻击者。这条有专门的用例与变异验证。 顺带删掉 internal/route/assets(webp + embed),已无引用。 测试: - TestWriteBannedTextFallback:未配图 → 403 + text/plain + 13 字节明文; - TestWriteBannedServesConfiguredImage:配了 1×1 PNG → image/png(按内容嗅探)、 Content-Length 与文件一致、body 与文件逐字节相同; - TestLoadBanImageRejectsBadInput:不存在 / 是目录 / 非图片 / 空文件四种配置错误 都不得装载,且**不得把文件内容当响应体吐出去**; - TestLoadBanImageTooLarge:超 2 MB 拒绝; - TestWriteBannedHead:两条分支的 HEAD 都只回头不给 body。 变异验证:去掉 image/* 校验 → 用例报「把文件内容当响应体吐了出去: "[server]\nsalt = secret"」;强制走纯文字分支 → 配图用例按预期变红。 实测:真 webp 嗅探为 image/webp;app.sample.ini 嗅探为 text/plain 被拒。
This commit is contained in:
@@ -179,3 +179,29 @@ Tomcat 的 skin 忘了 css(按钮白字无底色)、WP 登录页底部挂着
|
||||
- 换图方式:覆盖同目录 403.webp → 重新编译,无配置项。
|
||||
- 体积代价:每封禁请求 8.6 KB(原 13 字节)。被封来源重试频繁时要留意带宽,
|
||||
已压到 256×256;别塞几百 KB 原图。
|
||||
|
||||
### 关于「403 图放 R2 是否更好」的结论
|
||||
|
||||
结论:**保持 embed**。关键事实是「服务端代理/重定向并不省我们带宽」——
|
||||
字节最终还是从本机发给被封客户端;想真省只能让客户端自己去 R2 取,
|
||||
那就要放弃「403 直接是图片」,改成 403 + 一个 `<img src=外链>` 的 HTML。
|
||||
而本项目 R2 目前只有预签名 URL(7 天过期),长期用必须开 r2.dev 或绑自定义域
|
||||
(docs/theme-new-sakura.md 已记:直连 `*.r2.cloudflarestorage.com` 会 403)。
|
||||
8.6 KB 的单张图不值得引入外部依赖;被 ban 的多是脚本,根本不会加载图片。
|
||||
若动机只是「换图不想重编译」,加 `[security] ban_image_path` 磁盘覆盖更划算。
|
||||
|
||||
### 改为 ban_image_path(配置驱动,默认纯文字)
|
||||
|
||||
按用户要求撤掉 embed:`internal/route/assets/`(含 403.webp)已删除,
|
||||
行为改为「配了合法图 → 回图;留空 / 读不到 / 不是图 / >2MB → 纯文字 403」。
|
||||
- `[security] ban_image_path`(相对路径以进程工作目录为基准);
|
||||
- `loadBanImage()` 在 `route.New()` 里启动时读一次 → `atomic.Pointer[banResponse]`,
|
||||
热路径零磁盘 IO;任何失败只 Warn 不改启动(封禁响应是最后一道兜底,不能变 500);
|
||||
- **只认内容不认扩展名**(`http.DetectContentType` 必须是 `image/*`):配成
|
||||
app.ini/.env 会被拒绝,否则等于把配置文件吐给攻击者(变异验证过这条);
|
||||
- 上限 2 MB(被封来源反复重试,大图放大出网带宽);`Content-Length` 显式给;
|
||||
HEAD 不写 body;`no-store`。
|
||||
- 实测 sniff:真 webp → `image/webp`;`app.sample.ini` → `text/plain`(被拒)。
|
||||
|
||||
**教训**:同一功能改了两版(先 embed 后配置化),第二版要把上一版的资源一起清掉,
|
||||
否则留一个没人引用的 `assets` 包。`go vet ./...` 不报、测试也不报(该包仍被旧测试引用时)。
|
||||
|
||||
Reference in New Issue
Block a user