mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-26 20:33:15 +00:00
fix(webui): 限流来源识别改为「只信任受信反代的 XFF」—— 修复把自己锁在门外
★ 这是在生产上亲手踩出来的:上一提交加了按 IP 限流后,我用 8 次错误登录 做验证,结果**把管理员自己锁在外面 10 分钟**。 ## 现场证据 [webui] POST /api/v1/login from=127.0.0.1 auth=none status=429 webui 经 frp/nginx 穿透到公网时,**所有外部请求的 RemoteAddr 都是 127.0.0.1**。于是所有人共用一个桶:任何人爆破 5 次,就把**所有人** (含管理员)一起锁死。限流从防护变成了 DoS。 ## 我第一版还犯了个方向的错 当时我刻意**不采信** X-Forwarded-For,理由是「该头可伪造,换个头就能 绕过限流」。这个理由本身对,但结论下反了:完全不采信,在穿透部署下 **必然退化成全局限流** —— 而全局限流正是我试图避免的那个后果。 ## 正确做法:中间路线 **只信任受信反代发来的 XFF**。判定「是否来自受信反代」不能靠 内网/回环 IP 猜 —— 穿透部署下反代恰恰就在本机 127.0.0.1,与直连请求 完全同源,猜不出来。所以由部署方**显式声明**(新设置项 `webui.trusted_proxies`,逗号分隔 CIDR 或裸 IP)。 权衡写明:未声明时穿透明场景下限流退化为「全局」。这是**刻意的保守 默认** —— 宁可限流偏保守,也不能因为采信伪造头而形同虚设。 ## 判据(+4) - TestLoginRateLimitUsesForwardedForFromTrustedProxy:受信反代下按真实 客户端 IP 隔离(否则就是全局锁) - TestLoginRateLimitIgnoresUntrustedForwardedFor:换 XFF 头不得绕过限流 - TestLoginRateLimitNeedsExplicitTrustedProxyConfig:未配置 = 不采信 - TestParseTrustedProxies:合法项接受、非法项丢弃、空 = nil 全量:35 包全绿。 ★ 附带教训(也记在判据注释里):**用失败注入做验证时要意识到副作用 范围**。我那次「跑 8 次错误密码看看会不会限流」本身是合理的验证动作, 但它作用在**生产实例**上,且限流的作用域(全局化)正好覆盖了自己。 在带状态的安全机制上做破坏性验证,判据应该先证明作用域是对的。
This commit is contained in:
@ -234,6 +234,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{Key: "addr", Default: ":8080", Type: "string", DisplayName: "监听地址", Description: "Web 控制台监听地址", Category: "webui"})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{Key: "history_file", Default: "", Type: "string", DisplayName: "聊天记录文件", Description: "聊天记录存放路径。留空 = <data>/webui_chat_history.json;相对路径按 data 目录解析(可指向独立挂载盘)", Category: "webui"})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{Key: "trusted_proxies", Default: "", Type: "string", DisplayName: "受信反代网段", Description: "逗号分隔的 CIDR 或裸 IP(如 127.0.0.1,10.0.0.0/8)。只有来自这些网段的请求,其 X-Forwarded-For 才被采信用于登录限流计数。**经 frp/nginx 穿透到公网时必须配置**(反代通常就在本机 127.0.0.1),否则所有外部访问被视为同一来源,限流会误伤所有人。留空 = 不采信任何 XFF(保守默认)", Category: "webui"})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{Key: "api_key", Default: "", Type: "password", DisplayName: "API 密钥", Description: "访问 API 时需要的密钥", Category: "webui"})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{Key: "username", Default: "admin", Type: "string", DisplayName: "登录用户名", Description: "Web 控制台登录用户名", Category: "webui"})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{Key: "password", Default: "", Type: "password", DisplayName: "Web 控制台登录密码", Description: "Web 控制台登录密码", Category: "webui"})
|
||||
@ -302,6 +303,10 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
InvalidateProxyRoutes()
|
||||
|
||||
p.handler = NewHandler(s)
|
||||
// 受信反代网段:限流要按真实客户端隔离,就得以可信方式拿到客户端 IP。
|
||||
// 穿透部署下反代通常就在本机 127.0.0.1(外部请求的 RemoteAddr 全是它),
|
||||
// 不配置的话所有人共用一个桶,限流会误伤所有人 —— 生产上踩过。
|
||||
p.handler.trustedProxies = parseTrustedProxies(settingString(s.Settings(), "trusted_proxies"))
|
||||
p.handler.RegisterRoutes(p.mux)
|
||||
|
||||
// 最外层套 logged 中间件:记录每个请求的来源 IP / 方法 / 路径 / 认证方式 / 状态码。
|
||||
|
||||
Reference in New Issue
Block a user