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:
@@ -148,6 +148,30 @@ async function main() {
|
||||
check('只读模式下面板数据接口仍可用', r5.status === 200, `status=${r5.status}`);
|
||||
srvB.kill();
|
||||
|
||||
// ---------- D2:接口不得泄露私有层密钥 ----------
|
||||
console.log('\n[D2] 配置接口的密钥泄露');
|
||||
// 私有层放一个可识别的假 key:/api/monitors 无鉴权,绝不能把它回吐给面板或访客
|
||||
const SECRET = 'urFAKE-0123456789abcdefSECRET';
|
||||
const pubCfg = path.join(TMP, 'secret-pub.json');
|
||||
const privCfg = path.join(TMP, 'secret-priv.json');
|
||||
fs.writeFileSync(pubCfg, JSON.stringify({ settings: { uptimeRobot: { enabled: false, apiKey: '' } }, monitors: [] }, null, 2));
|
||||
fs.writeFileSync(privCfg, JSON.stringify({ settings: { uptimeRobot: { enabled: true, apiKey: SECRET } } }, null, 2));
|
||||
const srvC = startProber([`--config=${pubCfg}`, `--local-config=${privCfg}`, `--out=${path.join(TMP, 'c.json')}`], { PORT: '8896' });
|
||||
await waitForHttp('http://127.0.0.1:8896/api/status');
|
||||
|
||||
const mResp = await fetch('http://127.0.0.1:8896/api/monitors');
|
||||
const mText = await mResp.text();
|
||||
check('/api/monitors 不回吐私有层的 API Key', !mText.includes(SECRET) && !mText.includes('FAKE'), mText.includes(SECRET) ? '泄露了!' : '未泄露');
|
||||
const mJson = JSON.parse(mText);
|
||||
check('/api/monitors 的 uptimeRobot.apiKey 被剥离', !('apiKey' in (mJson.settings?.uptimeRobot ?? {})), JSON.stringify(mJson.settings?.uptimeRobot));
|
||||
check('/api/monitors 仍返回非敏感配置(面板要读)', typeof mJson.settings?.uptimeRobot?.enabled === 'boolean' && Array.isArray(mJson.monitors), `enabled=${mJson.settings?.uptimeRobot?.enabled}`);
|
||||
|
||||
const sText = await (await fetch('http://127.0.0.1:8896/api/status')).text();
|
||||
check('/api/status 快照不含 API Key', !sText.includes(SECRET), '');
|
||||
const pubText = await (await fetch('http://127.0.0.1:8896/monitors.json')).text();
|
||||
check('静态 monitors.json 不含私有层内容', !pubText.includes(SECRET), '');
|
||||
srvC.kill();
|
||||
|
||||
// ---------- E:来源判定 ----------
|
||||
console.log('\n[E] 来源判定');
|
||||
const { isPrivateAddress } = await import(`file://${PROBER.replace(/\\/g, '/')}`);
|
||||
|
||||
@@ -224,6 +224,20 @@ async function main() {
|
||||
check('/api/status 正常提供(首次无数据时返回空 state 并后台补跑)', st.status === 200 && typeof stBody === 'object' && stBody.monitors !== undefined, `HTTP ${st.status},monitors=${stBody.monitors ? Object.keys(stBody.monitors).length : '无'}`);
|
||||
check('首次访问触发后台探测(waitUntil 被调用)', pending.length > 0, `${pending.length} 个后台任务`);
|
||||
|
||||
// 配置接口无鉴权,绝不能回吐 API Key
|
||||
group('[E] 配置接口不得泄露密钥');
|
||||
const SECRET = 'urFAKE-0123456789abcdefSECRET';
|
||||
const envSecret = makeEnv({ uptimeRobot: { enabled: true, apiKey: SECRET } }, LOCAL_TARGETS);
|
||||
const mResp = await worker.default.fetch(new Request('https://x/api/monitors'), envSecret, { waitUntil() {} });
|
||||
const mText = await mResp.text();
|
||||
check('/api/monitors 不回吐 API Key', !mText.includes(SECRET), mText.includes(SECRET) ? '泄露了!' : '未泄露');
|
||||
const mJson = JSON.parse(mText);
|
||||
check('uptimeRobot.apiKey 被剥离', !('apiKey' in (mJson.settings?.uptimeRobot ?? {})), JSON.stringify(mJson.settings?.uptimeRobot));
|
||||
check('仍返回非敏感配置(面板要读)', typeof mJson.settings?.uptimeRobot?.enabled === 'boolean' && Array.isArray(mJson.monitors), `enabled=${mJson.settings?.uptimeRobot?.enabled}`);
|
||||
// 但 Worker 内部探测仍要能用 key 同步 UR —— 用同一份配置跑一轮确认不报错
|
||||
const snapSecret = await worker.__test.runChecks(envSecret);
|
||||
check('内部探测仍能读到 key 并同步 UR(剥离只作用于对外响应)', snapSecret.sources.uptimerobot.enabled === true, JSON.stringify(snapSecret.sources.uptimerobot).slice(0, 60));
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
urSrv.close();
|
||||
httpSrv.close();
|
||||
|
||||
Reference in New Issue
Block a user