fix: 管理入口改为「填链接」而非绑定特定隧道方案,并修掉配置接口的密钥泄露

用户反馈:「你不需要帮我处理 tunnel 这部分,也许我不是通过 tunnel 开放内网呢?
你只需要给我一个填写开放的管理端链接的地方。」

之前把「Cloudflare Tunnel」当成既定方案写进了配置与文档,这是替用户做了假设。
现在改为:项目只提供一个填地址的地方,怎么暴露完全由使用者决定。

- 删除 edge/cloudflare/tunnel.yml 与全部 tunnel 相关文档
- 新增 settings.adminUrl:管理界面里填一个地址,公开面板页脚出现「管理」按钮
  指向它;留空则完全不显示(默认,避免在公网面板暴露管理面位置)
- 只接受 http/https,javascript: 等协议被服务端拒绝(会被渲染成 <a href>)
- 设置落在 local.monitors.json(私有层),只有自己的面板会出现该按钮
- 面板启动时额外拉 /api/monitors 合并 settings:原先只读静态 monitors.json
  (公开层),导致私有层的设置面板看不到;纯静态部署无该接口时沿用公开配置

顺带修掉一个既存漏洞(与本轮改动无关,是引入私有层叠加机制时埋下的):
/api/monitors 无鉴权却回吐合并后的完整配置,包含私有层的 UptimeRobot API Key ——
只要配了 key,任何访客 curl /api/monitors 就能拿到。现在两侧(Node 与 Worker)
都对对外响应剥离 uptimeRobot.apiKey,保留 enabled 等非敏感字段供面板读取;
探针内部同步仍用完整配置,剥离只作用于 HTTP 出口。

测试:verify-worker 36→40 项(新增 [E] 配置接口密钥泄露 4 条)、
verify-security 20→25 项(新增 [D2] 密钥泄露 5 条)。全项目 8 套 314 项通过。
This commit is contained in:
tamakyi
2026-10-14 15:23:22 +08:00
parent d2aa0a1163
commit f345626840
16 changed files with 480 additions and 379 deletions
+25
View File
@@ -138,6 +138,7 @@ export function createAdminRouter({ auth, localConfigPath, reloadConfig, runtime
hideTargets: s.hideTargets === true,
notifyEnabled: notifyConfig(cfg).enabled,
tlsEnabled: tlsConfig(cfg).enabled,
adminUrl: s.adminUrl ?? '',
},
maintenance: Array.isArray(s.maintenance) ? s.maintenance : [],
monitors: cfg.monitors.map((m) => ({
@@ -300,6 +301,30 @@ export function createAdminRouter({ auth, localConfigPath, reloadConfig, runtime
return sendJson(res, { ok: true, dataFilter: filter });
}
// 面板上的管理入口链接:只存地址,不做任何暴露判断(怎么暴露由使用者自己决定)
if (p === '/api/admin/admin-url' && req.method === 'POST') {
const parsed = await readJsonBody(req, res);
if (!parsed.ok) return;
const body = parsed.value;
const raw = String(body.adminUrl ?? '').trim();
if (raw) {
// 只收 http/https:面板会把它渲染成 <a href>,javascript: 之类必须挡住
let u;
try { u = new URL(raw); } catch { return sendJson(res, { error: 'invalid-url', hint: '不是合法的绝对 URL' }, { status: 400 }); }
if (u.protocol !== 'http:' && u.protocol !== 'https:') {
return sendJson(res, { error: 'invalid-url', hint: '只支持 http/https' }, { status: 400 });
}
}
savePrivateConfig(localConfigPath, (local) => {
local.settings = { ...(local.settings ?? {}), adminUrl: raw };
return local;
});
reloadConfig();
scheduleWrite(runtime);
console.log(`[admin] 面板管理入口链接${raw ? `已设置为 ${raw}` : '已清空'}(来自 ${ip})`);
return sendJson(res, { ok: true, adminUrl: raw });
}
if (p === '/api/admin/probe-now' && req.method === 'POST') {
await Promise.race([forceRound(runtime), new Promise((r) => setTimeout(r, forcedMaxWaitMs))]);
if (res.writableEnded || res.destroyed) return;