feat: v0.12.2 —— 跨域只读支持(面板可部署在别处)

用户诉求:管理端与前端完全分离,本地探针只跑 /admin,前端部署到 GitHub Pages
并硬编码管理地址,探测仍在本地常驻。用户自己搭隧道给本地套 HTTPS。

硬限制(务必先看):GitHub Pages 强制 HTTPS,而浏览器「混合内容」策略禁止
HTTPS 页面 fetch HTTP 资源 —— 这与 CORS 无关,改设置也没用。
因此 settings.dataUrl 必须与面板页面同为 HTTPS。

服务端
- 只读接口(/api/status、/api/monitors、/api/events、/api/v2/getMonitors、
  /api/refresh)按 settings.corsOrigins 白名单回显 Access-Control-Allow-Origin
- /api/admin/* 一律不放行跨域,预检直接 403 —— 外部页面既读不到也改不了
- 绝不使用 '*':白名单里写 '*' 不放行任何来源

前端
- settings.dataUrl 存在时,所有 api/ 相对路径自动改写到该地址
- 数据源标记为「远程探针(跨域)」,页脚可区分
- 管理界面新增两项配置:跨域数据源地址、允许跨域的页面地址(改完立即生效)

关键设计:origin 匹配必须考虑端口
origin 是「协议 + 主机 + 端口」的完整组合。最初白名单存 http://localhost(无端口),
导致本地前端跑在 localhost:5173 时被拒 —— 浏览器实测 Failed to fetch 而 curl 能过。
修法:回环主机(localhost / 127.0.0.1)忽略端口放行,其余要求完整 origin 精确匹配。

测试:verify-security 25→40 项,新增 [D3] CORS 白名单 14 条 + [D4] '*' 写法 1 条。
全项目 8 套 334 项通过。

附记一个测试上的教训:跨域 fetch 时 JS 读不到 Access-Control-Allow-Origin
(CORS 规范规定只有同源能读),所以断言「是否放行」要看 fetch 有没有拿到数据,
不能看响应头 —— 我一开始用 headers.get() 判断,拿到 null 误以为失败。
This commit is contained in:
tamakyi
2026-10-14 16:19:31 +08:00
parent f345626840
commit 6b0a23c38e
16 changed files with 835 additions and 575 deletions
+17
View File
@@ -180,6 +180,23 @@ v0.12.0 起,所有会改配置的操作都放在 `/admin`,**必须绑定并
`/api/monitors` 与 Worker 上的同名接口都**无鉴权**,因此对外响应一律剥离 `uptimeRobot.apiKey`(保留 `enabled` 等非敏感字段供面板读取)。探针内部同步 UptimeRobot 仍使用完整配置,剥离只作用于 HTTP 出口。`/api/status` 与静态 `monitors.json` 同样不含私有层内容。
### 跨域只读白名单(CORS)
面板部署在别处(GitHub Pages / Cloudflare)、探针在另一台机器时,需要让指定页面跨域读取状态与配置。配置项:`settings.dataUrl`(面板去哪里取数据)与 `settings.corsOrigins`(允许哪些页面读取)。
核心风险是「任意网站都能读走监控数据」,措施:
| 风险 | 措施 |
| --- | --- |
| 开放成 `*` 导致任意网站可读 | **代码里不存在 `*` 分支**:白名单里写 `*` 不会放行任何来源(已加断言) |
| 本机回环被任意端口冒用 | 默认只放行 `localhost` / `127.0.0.1` 且**忽略端口**(本地前端常跑 5173);其余必须精确匹配完整 origin |
| 前缀伪造域名(`https://you.github.io.evil.com`) | 精确字符串匹配,不做子串判断(已加断言) |
| 协议降级(`https://localhost` 冒用 http 的默认项) | 协议必须一致才走回环豁免(已加断言) |
| 中间缓存串味(把 A 的响应给 B) | 回显具体 origin 并带 `Vary: Origin` |
| **外部页面读取或改写管理端** | `/admin` 与 `/api/admin/*` **一律不带 `Access-Control-*` 头**,预检直接 403(已加断言) |
| 外部页面借 CORS 触发有副作用的调用 | `/api/admin/*` 完全不放行;`/api/refresh` 另有 `refreshDecision` 的限流与 `refreshLocalOnly` 兜底 |
| 跨域读到密钥 | 跨域响应与同源响应走同一套剥离逻辑,`uptimeRobot.apiKey` 不会回吐(已加断言) |
### 威胁模型与对应措施
| 威胁 | 措施 |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 265 KiB

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 452 KiB

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 KiB

After

Width:  |  Height:  |  Size: 215 KiB