被封禁来源访问任何路径时,原来只回 13 字节的 "403 Forbidden":既没有信息量, 人肉访问(尤其是共享出口被连累的误封)时看着像站点挂了;扫描/爆破脚本也常靠 读这行明文确认「自己已被拦」。 改为返回一张 256×256 的 webp(约 8.6 KB,画面写着 403 Forbidden),状态码不变: - 图片用 //go:embed 编进二进制,不依赖外部 URL、不占公开路径; - 显式给 Content-Length(8 KB 超过 Go 的 2 KB 写缓冲,不设会退回 chunked); - Cache-Control: no-store —— 中间层缓存 403 没有意义,还会让解封后的响应继续命中; - HEAD 只回头不写 body; - 放在 internal/route/assets/ 而不是 static/:static 下的东西有可公开访问的 URL, 等于多一个指纹入口。 注意链接:给的 R2 预签名地址 X-Amz-Expires=604800(7 天),热链到期就会挂, 所以是把字节取回来打包,而不是重定向或代理。 测试: - TestWriteBanned:状态码 403、image/webp、Content-Length 与内嵌字节数一致、 body 前 12 字节是 RIFF/WEBP(防止被换成 HTML 或明文这类回归); - TestWriteBannedHead:HEAD 无 body 但有 Content-Length; - 原 TestHoneypotBansEvenWhenAutoBanDisabled 里断言 "403 Forbidden" 明文的用例 改为断言 image/webp 与体积。 变异验证:把 writeBanned 改回写明文 + text/plain,两条用例均按预期变红。
35 lines
1.5 KiB
Go
35 lines
1.5 KiB
Go
package route
|
||
|
||
import (
|
||
"net/http"
|
||
"strconv"
|
||
|
||
"github.com/tamakyi/TamaBox/internal/route/assets"
|
||
)
|
||
|
||
// writeBanned 写出「来源已被封禁」的响应。
|
||
//
|
||
// 状态码仍然是 403 —— 语义不变,只把 body 从 13 字节的 "403 Forbidden"
|
||
// 换成一张 256×256 的 webp(约 8.6 KB):
|
||
// - 顶掉明文:爆破/扫描脚本不少会读响应体里的 "403 Forbidden" 来确认自己
|
||
// 已被拦,换掉 body 至少让这类判断失效(状态码本身还是照常返回);
|
||
// - 给人看:误封(比如共享出口、代理)时打开浏览器看到一张写着 403 的图,
|
||
// 比一个没有任何样式的白屏好判断得多。
|
||
//
|
||
// 图片是编译期 embed 的,不走磁盘 IO,也不占一个可公开访问的 URL。
|
||
// 被封住的来源往往反复重试,所以这里显式给 Content-Length(否则 8 KB 会退回
|
||
// chunked),并标 no-store:中间层缓存一张 403 图没有意义,还会让解封后
|
||
// 的响应继续命中缓存。
|
||
func writeBanned(w http.ResponseWriter, method string) {
|
||
h := w.Header()
|
||
h.Set("Content-Type", "image/webp")
|
||
h.Set("Content-Length", strconv.Itoa(len(assets.BannedImage)))
|
||
h.Set("Cache-Control", "no-store, no-cache, must-revalidate")
|
||
h.Set("X-Robots-Tag", "noindex, nofollow")
|
||
w.WriteHeader(http.StatusForbidden)
|
||
// HEAD 不写 body(net/http 也会丢弃,这里省掉一次无意义的拷贝)。
|
||
if method != http.MethodHead {
|
||
_, _ = w.Write(assets.BannedImage)
|
||
}
|
||
}
|