From 5e4e488fe36b9e2841da18a62b59fc2c67b958e3 Mon Sep 17 00:00:00 2001 From: kylin Date: Fri, 17 Apr 2026 15:51:16 +0800 Subject: [PATCH 1/2] fix: read CF API credentials from POST body instead of URL params The /admin/getCloudflareUsage endpoint accepted GlobalAPIKey and APIToken as GET query parameters. Sensitive credentials in URLs are recorded in: - Cloudflare Workers request logs - Browser history / address bar autocomplete - Referrer headers sent to third-party resources on the admin page - Proxy / CDN access logs Change: credentials are now read from a JSON POST body first. GET query parameters are kept as a backwards-compatible fallback so existing callers are not broken while the frontend is updated. Frontend callers should migrate to POST with JSON body: { Email, GlobalAPIKey, AccountID, APIToken } --- _worker.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/_worker.js b/_worker.js index ab07ab55ac..605868f2c6 100644 --- a/_worker.js +++ b/_worker.js @@ -81,7 +81,21 @@ export default { return new Response(读取日志内容, { status: 200, headers: { 'Content-Type': 'application/json;charset=utf-8' } }); } else if (区分大小写访问路径 === 'admin/getCloudflareUsage') {// 查询请求量 try { - const Usage_JSON = await getCloudflareUsage(url.searchParams.get('Email'), url.searchParams.get('GlobalAPIKey'), url.searchParams.get('AccountID'), url.searchParams.get('APIToken')); + // 优先从 POST body 读取凭据(避免敏感信息暴露在 URL / 服务器日志中) + // 兼容旧版 GET query 参数作为降级方案 + let Email, GlobalAPIKey, AccountID, APIToken; + if (request.method === 'POST') { + try { + const body = await request.json(); + Email = body.Email; GlobalAPIKey = body.GlobalAPIKey; + AccountID = body.AccountID; APIToken = body.APIToken; + } catch (_) { } + } + Email = Email ?? url.searchParams.get('Email'); + GlobalAPIKey = GlobalAPIKey ?? url.searchParams.get('GlobalAPIKey'); + AccountID = AccountID ?? url.searchParams.get('AccountID'); + APIToken = APIToken ?? url.searchParams.get('APIToken'); + const Usage_JSON = await getCloudflareUsage(Email, GlobalAPIKey, AccountID, APIToken); return new Response(JSON.stringify(Usage_JSON, null, 2), { status: 200, headers: { 'Content-Type': 'application/json' } }); } catch (err) { const errorResponse = { msg: '查询请求量失败,失败原因:' + err.message, error: err.message }; From 2bfadc201ccba781d56ca0d031b980f362960802 Mon Sep 17 00:00:00 2001 From: kylin Date: Fri, 17 Apr 2026 18:19:49 +0800 Subject: [PATCH 2/2] fix: address Copilot review comments on PR #1126 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three issues raised by the automated review: 1. Deprecation signal for legacy GET params When credentials are supplied via URL query parameters (the old, insecure path), the JSON response now includes a '_warning' field instructing callers to migrate to POST body. This keeps the fallback functional while actively nudging migration. 2. Gate JSON parsing on Content-Type check Previously request.json() was called for any POST body and parse errors were silently swallowed, masking malformed requests and relying on exceptions for control flow. Now parsing only runs when Content-Type includes 'application/json', falling back to query params (with deprecation warning) for other content types. 3. Normalize body values to strings Fields read from the JSON body (Email, GlobalAPIKey, AccountID, APIToken) are now coerced to String via String() before being passed to getCloudflareUsage(). This prevents a type mismatch when AccountID arrives as a JSON number — the Cloudflare GraphQL API declares it as String! and rejects non-string inputs. --- _worker.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/_worker.js b/_worker.js index 605868f2c6..1afc259116 100644 --- a/_worker.js +++ b/_worker.js @@ -82,20 +82,29 @@ export default { } else if (区分大小写访问路径 === 'admin/getCloudflareUsage') {// 查询请求量 try { // 优先从 POST body 读取凭据(避免敏感信息暴露在 URL / 服务器日志中) - // 兼容旧版 GET query 参数作为降级方案 + // 兼容旧版 GET query 参数作为降级方案(已废弃,未来版本将移除) let Email, GlobalAPIKey, AccountID, APIToken; - if (request.method === 'POST') { + let usingDeprecatedQueryParams = false; + if (request.method === 'POST' && (request.headers.get('content-type') || '').includes('application/json')) { try { const body = await request.json(); - Email = body.Email; GlobalAPIKey = body.GlobalAPIKey; - AccountID = body.AccountID; APIToken = body.APIToken; + // 统一转换为字符串,防止 GraphQL String! 类型校验失败 + Email = body.Email != null ? String(body.Email) : undefined; + GlobalAPIKey = body.GlobalAPIKey != null ? String(body.GlobalAPIKey) : undefined; + AccountID = body.AccountID != null ? String(body.AccountID) : undefined; + APIToken = body.APIToken != null ? String(body.APIToken) : undefined; } catch (_) { } } - Email = Email ?? url.searchParams.get('Email'); - GlobalAPIKey = GlobalAPIKey ?? url.searchParams.get('GlobalAPIKey'); - AccountID = AccountID ?? url.searchParams.get('AccountID'); - APIToken = APIToken ?? url.searchParams.get('APIToken'); + // 降级到 query 参数(已废弃) + if (!Email && !GlobalAPIKey && !AccountID && !APIToken) { + Email = url.searchParams.get('Email') ?? undefined; + GlobalAPIKey = url.searchParams.get('GlobalAPIKey') ?? undefined; + AccountID = url.searchParams.get('AccountID') ?? undefined; + APIToken = url.searchParams.get('APIToken') ?? undefined; + if (Email || GlobalAPIKey || AccountID || APIToken) usingDeprecatedQueryParams = true; + } const Usage_JSON = await getCloudflareUsage(Email, GlobalAPIKey, AccountID, APIToken); + if (usingDeprecatedQueryParams) Usage_JSON._warning = 'Passing credentials via URL query parameters is deprecated and will be removed in a future version. Please switch to POST with a JSON body.'; return new Response(JSON.stringify(Usage_JSON, null, 2), { status: 200, headers: { 'Content-Type': 'application/json' } }); } catch (err) { const errorResponse = { msg: '查询请求量失败,失败原因:' + err.message, error: err.message };