fix: read CF API credentials from POST body to prevent URL-based leakage#1126
fix: read CF API credentials from POST body to prevent URL-based leakage#1126kasc0206 wants to merge 2 commits intocmliu:mainfrom
Conversation
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 }
There was a problem hiding this comment.
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,APITokenfrom POST JSON body whenrequest.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.
| Email = Email ?? url.searchParams.get('Email'); | ||
| GlobalAPIKey = GlobalAPIKey ?? url.searchParams.get('GlobalAPIKey'); | ||
| AccountID = AccountID ?? url.searchParams.get('AccountID'); | ||
| APIToken = APIToken ?? url.searchParams.get('APIToken'); |
There was a problem hiding this comment.
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.
| try { | ||
| const body = await request.json(); | ||
| Email = body.Email; GlobalAPIKey = body.GlobalAPIKey; | ||
| AccountID = body.AccountID; APIToken = body.APIToken; | ||
| } catch (_) { } |
There was a problem hiding this comment.
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.
| 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; | |
| } |
| 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); |
There was a problem hiding this comment.
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.
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.
Fixes #1121
The
/admin/getCloudflareUsageendpoint acceptedGlobalAPIKeyandAPITokenas GET query parameters. These values appear in:Refererheaders forwarded to any third-party resource on the admin pageGlobalAPIKeyis 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: