Skip to content

fix: 修复独立审计发现的两个高风险缺陷(H1 凭证写入竞态 / H2 管理接口默认开放) - #6

Merged
C3H3-AI merged 1 commit into
masterfrom
fix/audit-h1-h2
Sep 15, 2026
Merged

C3H3-AI merged 1 commit into
masterfrom
fix/audit-h1-h2

Conversation

@C3H3-AI

@C3H3-AI C3H3-AI commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Description / 变更描述

修复独立审计(AUDIT-2026-09-15.md)发现的两个高风险缺陷
审计不依赖 TECHNICAL-DOC.md 的既有结论,每条均经代码定位或可复现实验验证。


🔴 H1 跨进程并发写凭证,会永久丢失 refreshToken

位置src/internal/auth/auth.go:204(及 pool.go 的 state 文件)

原实现用固定临时文件名:

tmp := a.FilePath + ".tmp"

Auth.mu 只保护进程内,而实际是多进程架构:

进程 触发路径 位置
serverd 请求前预刷新 handler.go:472
serverd 定时保活 scheduler.go:374/414
ctl(独立进程) 面板点「刷新令牌」 ctl/main.go:317

两进程各有独立的内存副本与锁,进程间无互斥,且写同一个 tmp 文件。

实测(并发 800 次写入):旧实现失败 292 次。

更严重的是逻辑丢失 —— 上游 refresh 会轮换 refreshToken(旧的即失效):

落盘的 refreshToken: RT-1
→ 真正的 RT-2 已丢失
→ RT-1 已被上游轮换失效 → 下次刷新失败 → 需重新登录

(已确定性复现)

文件本身不会损坏rename 是原子的),丢的是「较新的那个值」。

修复:新增 internal/atomicfile

  • 临时文件改用 os.CreateTemp 生成唯一名 → 消除写交错与 rename 竞争
  • 写入前对目标路径加跨进程 flock(unix;非 unix 退化为无锁,仅保证可交叉编译)
  • 写入后 fsync 文件与目录

auth.gopool.go(state 文件同理)一并改用。


🔴 H2 面板默认无登录,管理接口对整个内网开放

链路

config.yaml 默认 webui_user/pass 为空
  → load_options() 中 "" 不是 None,会【覆盖】掉 DEFAULT_OPTIONS 的 admin/admin
  → _webui_enabled() == False
  → _mgmt_authorized() 回退到 _is_local()
  → _is_local() 对 172.30.* / 172.16.* / 192.168.* / 10.* 一律放行
  → 而 config.yaml 有 ports: 7870/tcp: 7870(映射到宿主机)

后果:局域网内任意设备访问 http://<HA>:7870/api/* 即可无认证
调用管理接口(账号列表、刷新令牌、修改配置)。

根因:ingress 转发(172.30.x)与 LAN 直连(192.168.x)的
源 IP 都是内网,仅凭 IP 段无法区分

修复:拆分判断维度

  • _is_loopback() 仅回环
  • _is_private() 内网段(保留,注释说明其不可用于鉴权
  • _is_ingress()/api/hassio_ingress/ 路径前缀识别(与既有 login_ui.py:1081 的同款判断一致)
  • _mgmt_authorized() 未启用登录时只信任 ingress 与本机回环

同时移除已无调用点的 _is_local(),避免误用。


其他

  • .gitignore 增加 __pycache__/*.pyc,并移除已误入库的 .pyc
  • 新增 AUDIT-2026-09-15.md(完整审计报告)

验证

go build ./...   PASS
go vet ./...     PASS
go test ./...    PASS(含 atomicfile 4 个新单测)

scripts/test_mgmt_auth.py   11/11 通过

H1 对照实验(同一测试,旧实现 vs 新实现):

实现 并发 800 次写入失败数
旧(固定 tmp 名) 292
新(atomicfile) 0

H2 真值表:11 个用例覆盖 ingress / 回环 / LAN / 公网 /
已登录 / 启用登录各组合,全通过。关键差异:

来源
LAN 直连 192.168.x / 10.x True(开放 False(拒绝
ingress 转发 True True(保留)
本机回环 True True(保留)

⚠️ 本 PR 改动了 login_ui.py(89KB、此前零测试)。
已加 scripts/test_mgmt_auth.py 覆盖本次改动的鉴权逻辑,
但面板其余部分仍无测试覆盖。

Type of change / 变更类型

  • bugfix: Bug fix / 问题修复
  • new feature: New feature / 新功能
  • refactor: Code refactoring and optimization / 重构与优化
  • breaking: Breaking change / 破坏性变更
  • docs: Documentation / 文档更新

Testing / 测试

  • HA Version / HA版本: 2024.1.0+
  • Add-on Version / 加载项版本: 1.1.0b13
  • Test Result / 测试结果: 见上方验证章节;H1 有对照实验,
    H2 有真值表,Go 侧 build/vet/test 全绿

Checklist / 自检清单

  • Code follows project standards / 代码符合项目规范
  • No debug code or unrelated files / 无调试代码或无关文件

来源:AUDIT-2026-09-15.md 的独立审计(不依赖 TECHNICAL-DOC 既有结论)。

## H1 跨进程并发写凭证,会永久丢失 refreshToken

原实现用【固定】临时文件名:

    tmp := a.FilePath + ".tmp"

`Auth.mu` 只保护进程内,而实际是多进程架构:
  serverd —— 请求前预刷新(handler.go:472)、定时保活(scheduler.go:374/414)
  ctl     —— 独立子进程,面板点「刷新令牌」时写同一文件(ctl/main.go:317)

两进程各有独立的内存副本与锁,进程间无互斥,且写同一个 tmp 文件,于是:
  1) 内容交错;
  2) 一方先 rename 走,另一方 rename 时 tmp 已不存在。

实测(并发 800 次写入):旧实现失败 292 次。
更严重的是**逻辑丢失**:上游 refresh 会轮换 refreshToken,
两进程各拿到新值,后落盘者覆盖先落盘者;若落盘的是已被轮换失效的那个,
账号下次刷新即失败,需人工重登(已确定性复现)。

文件本身不损坏(rename 原子),丢的是「较新的那个值」。

修复:新增 internal/atomicfile 包
  - 临时文件改用 os.CreateTemp 生成唯一名(消除写交错与 rename 竞争)
  - 写入前对目标路径加跨进程 flock(unix;非 unix 退化为无锁,仅保证可交叉编译)
  - 写入后 fsync 文件与目录
auth.go 与 pool.go(state 文件同理)一并改用。

单测覆盖:并发写零错误、无残留临时文件、权限 0600、覆盖写不残留旧内容。

## H2 面板默认无登录,管理接口对整个内网开放

链路:
  config.yaml 默认 webui_user/pass 为空
  → load_options() 中 "" 不是 None,会**覆盖**掉 DEFAULT_OPTIONS 的 admin/admin
  → _webui_enabled() == False
  → _mgmt_authorized() 回退到 _is_local()
  → _is_local() 对 172.30.* / 172.16.* / 192.168.* / 10.* 一律放行
  → 而 config.yaml 有 ports: 7870/tcp: 7870(映射到宿主机)

后果:局域网内任意设备访问 http://<HA>:7870/api/* 即可无认证
调用管理接口(账号列表、刷新令牌、修改配置)。

根因:ingress 转发与 LAN 直连的**源 IP 都是内网**
(172.30.x vs 192.168.x),仅凭 IP 段无法区分。

修复:拆分判断维度
  - _is_loopback()  仅回环
  - _is_private()   内网段(保留,注释说明其不可用于鉴权)
  - _is_ingress()   按 /api/hassio_ingress/ 路径前缀识别(与既有第 1081 行判断一致)
  - _mgmt_authorized() 未启用登录时只信任 ingress 与本机回环,不再信任整个内网

同时移除已无调用点的 _is_local(),避免误用。

验证:scripts/test_mgmt_auth.py 真值表 11 个用例全通过,
覆盖 ingress / 回环 / LAN / 公网 / 已登录 / 启用登录各组合。

## 其他

- .gitignore 增加 __pycache__/ 与 *.pyc,并移除已误入库的 .pyc
- 新增 AUDIT-2026-09-15.md(完整审计报告,含与 TECHNICAL-DOC 的交叉比对)

## 验证

  go build ./...   PASS
  go vet ./...     PASS
  go test ./...    PASS(含 atomicfile 4 个新单测)
  scripts/test_mgmt_auth.py  11/11 通过
@github-actions github-actions Bot added the 🐛 Bug Fix Bug fix / 问题修复 label Sep 15, 2026
@C3H3-AI
C3H3-AI merged commit 37c6210 into master Sep 15, 2026
3 checks passed
@C3H3-AI C3H3-AI mentioned this pull request Sep 15, 2026
7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛 Bug Fix Bug fix / 问题修复

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant