Skip to content

fix: read CF API credentials from POST body to prevent URL-based leakage#1126

Open
kasc0206 wants to merge 2 commits intocmliu:mainfrom
kasc0206:fix/api-key-url-exposure
Open

fix: read CF API credentials from POST body to prevent URL-based leakage#1126
kasc0206 wants to merge 2 commits intocmliu:mainfrom
kasc0206:fix/api-key-url-exposure

Conversation

@kasc0206
Copy link
Copy Markdown

Fixes #1121

The /admin/getCloudflareUsage endpoint accepted GlobalAPIKey and APIToken as GET query parameters. These values appear in:

  • Cloudflare Workers request logs
  • Browser history / address-bar autocomplete
  • HTTP Referer headers forwarded to any third-party resource on the admin page

GlobalAPIKey is a Cloudflare account-level credential; leaking it can lead to full account compromise.

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 immediately broken.

Frontend callers should migrate to:

fetch("/admin/getCloudflareUsage", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ Email, GlobalAPIKey, AccountID, APIToken })
});

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 }
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR mitigates credential leakage for the /admin/getCloudflareUsage admin endpoint by preferring Cloudflare API credentials from a JSON POST body instead of URL query parameters, while keeping a backwards-compatible GET fallback.

Changes:

  • Read Email, GlobalAPIKey, AccountID, APIToken from POST JSON body when request.method === 'POST'.
  • Fall back to existing GET query parameters when POST body is missing/invalid or for legacy callers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread _worker.js Outdated
Comment on lines +94 to +97
Email = Email ?? url.searchParams.get('Email');
GlobalAPIKey = GlobalAPIKey ?? url.searchParams.get('GlobalAPIKey');
AccountID = AccountID ?? url.searchParams.get('AccountID');
APIToken = APIToken ?? url.searchParams.get('APIToken');
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still accepts credentials via GET query params as a fallback, which keeps the original leakage vector for any legacy callers. To help migration and reduce ongoing risk, consider adding an explicit deprecation signal when query params are used (e.g., a warning field in the JSON response or a Warning/Deprecation response header) so clients can detect and update.

Copilot uses AI. Check for mistakes.
Comment thread _worker.js
Comment on lines +88 to +92
try {
const body = await request.json();
Email = body.Email; GlobalAPIKey = body.GlobalAPIKey;
AccountID = body.AccountID; APIToken = body.APIToken;
} catch (_) { }
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For POST requests, this attempts request.json() unconditionally and silently ignores parse errors. That means non-JSON/empty/malformed bodies fall back to query params with no signal, and the common case relies on exceptions for control flow. Consider gating parsing on Content-Type: application/json (you already compute contentType earlier) and returning a 400 for invalid JSON when method is POST, instead of swallowing the error.

Suggested change
try {
const body = await request.json();
Email = body.Email; GlobalAPIKey = body.GlobalAPIKey;
AccountID = body.AccountID; APIToken = body.APIToken;
} catch (_) { }
const contentType = request.headers.get('Content-Type') || '';
if (contentType.toLowerCase().startsWith('application/json')) {
let body;
try {
body = await request.json();
} catch (err) {
const errorResponse = { msg: '请求体不是有效的 JSON', error: err.message };
return new Response(JSON.stringify(errorResponse, null, 2), { status: 400, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
}
Email = body.Email; GlobalAPIKey = body.GlobalAPIKey;
AccountID = body.AccountID; APIToken = body.APIToken;
}

Copilot uses AI. Check for mistakes.
Comment thread _worker.js
Comment on lines +89 to +98
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);
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Values read from JSON body may not be strings (e.g., AccountID could be a number). getCloudflareUsage sends AccountID to GraphQL as a String!, so a non-string value can cause the Cloudflare API to reject the request. Consider normalizing Email/GlobalAPIKey/AccountID/APIToken to strings (or validating types) before calling getCloudflareUsage.

Copilot uses AI. Check for mistakes.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security] Cloudflare API 凭据(GlobalAPIKey/APIToken)通过 GET URL 参数传输,存在泄露风险

2 participants