Files
HomeAgent/internal/plugins
JianFeeeee 5ebc4481b0 fix(webui): 登录入口加固 —— 限流 + 常量时间比对 + 请求体限量 + 防用户名枚举
门户可经 frp 穿透到公网(https://homeagent.jianfgit.xyz/ 实测直达),
而 handleLogin 原先是**零防护**:无限流、无失败计数、口令用 == 明文比对、
失败不审计。等于把唯一��口令入口直接开到外网任人爆破。

## 改动

1. **按来源 IP 的失败计数限流**(login_limiter.go)
   - 5 次失败后拦,10 分钟窗口。
   - 退避而非永久封禁:窗口过期自动恢复。永久封禁意味着一旦误撞
     (或被撞库)就再也登不进,只能上机器改配置。
   - 成功即清零:手滑输错几次不该被永久记账。
   - **刻意不采信 X-Forwarded-For** —— 该头可伪造,直接采信等于让
     攻击者换一个头就能绕过限流,甚至把限流当成打别人来源的武器。
     代价(已在注释写明):若 webui 挂在反代后,限流会退化成「全局」,
     那种部署应在反代层限流或用 PROXY protocol 传真实来源。
   - **刻意不做账号级锁定**:本系统只有一个管理员账号,账号级锁定
     相比 IP 级无额外收益,却多一个误伤面。
   - 过期记录会被 prune —— 否则攻击者轮换 IP 就能喂成内存泄漏。

2. **常量时间比对**(crypto/subtle):`==` 会在第一个不同字节处短路,
   泄漏「猜对了几位」的时序信息。

3. **请求体限量**:ContentLength 前置拒绝 + MaxBytesReader 兜底。
   ★ 后者**不能只靠解码器报错** —— json.Decoder 按需读流,遇到
   「超大 + 非法 JSON」会在第 0 字节就报语法错误、永远读不到上限,
   于是 8MB 数据已进缓冲而 MaxBytesError 从未出现。只挂 MaxBytesReader
   的写法对最省力的攻击载荷反而无效(实测确认)。

4. **防用户名枚举**:用户不存在与口令错误给完全相同的状态码与报文。

## 判据(8 条)

限流触发 / 按来源隔离(否则一个 IP 就能把所有人锁死,限流即 DoS)/
成功清零 / Retry-After / 请求体限量 / 防枚举 / 过期清理 / 重试时长非零。

变异验证(3 条打红后还原):
- 去掉限流调用 → 3 条判红
- 去掉 ContentLength 前置检查 → 判红(回到 400)
- 去掉 Reset → **起初没打红**:原判据「跑 30 次看是否限流」在阈值只有 5
  时无论有没有 Reset 都会限流,是条**假判据**。已改为**测出实际阈值**
  (清零后应重新拿到完整额度),再去变异即打红。

诚实说明:常量时间比对那条**无法用单测可靠断言**(时序属性,噪声远大于
信号)。它由代码评审保证,不由测试保证 —— 写明以免后人以为有测试兜着。
2026-09-26 13:49:59 +08:00
..
2026-07-21 21:07:37 +08:00