T05 · Unauthorized Access and Privilege Escalation
Error
- Location
- src/index.ts:9
- Finding
- Unauthenticated Caller Can Initiate Charges for Arbitrary User Identifiers<![CDATA[ ## Vulnerability Details **File Location**: `src/index.ts:9-24`, with the privileged outbound charge performed in `src/billing.ts:8-22` **Vulnerability Type**: Missing authentication and authorization on a billing operation **Risk Level**: High ### Vulnerable Code `src/index.ts:9-24`: ```ts async fetch(request: Request, env: Env): Promise<Response> { if (request.method !== "POST") { return Response.json({ error: "POST required" }, { status: 405 }); } const body = await request.json() as { user_id: string }; if (!body.user_id) { return Response.json({ error: "user_id required" }, { status: 400 }); } const billing = await chargeUser({ userId: body.user_id, apiKey: env.SKILLPAY_API_KEY, priceUsdt: 0.01, skillId: SKILL_ID, }); ``` `src/billing.ts:8-22`: ```ts try { const response = await fetchFn(SKILLPAY_API, { method: "POST", headers: { "Content-Type": "application/json", "X-API-Key": apiKey, }, body: JSON.stringify({ user_id: userId, skill_id: skillId, amount: priceUsdt, }), }); const data = await response.json(); return data as BillingResult; ``` ### Technical Analysis The public Cloudflare Worker accepts `user_id` directly from an unauthenticated request body and uses it to initiate a billing operation. It performs no caller authentication, authorization check, signed-request validation, or verification that the supplied identifier belongs to the requester. The Worker then elevates this untrusted input into a privileged request by authenticating to SkillPay with the server-side `SKILLPAY_API_KEY`. Consequently, possession of the Worker endpoint URL may be sufficient to request charges against arbitrary, guessed, or previously observed user identifiers. Checking only that `user_id` is non-empty does not establish ownership or authorization. The implementation also lacks replay protection, idempotency controls, and application-level rate limiting ...[truncated 2114 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. **Authenticate every caller** - Require a cryptographically verifiable session, token, or signed request before accepting a charge request. - Reject missing, expired, malformed, or incorrectly signed credentials. 2. **Derive the billing identity from verified credentials** - Do not trust `user_id` supplied in request JSON. - Resolve the SkillPay user identifier from the authenticated principal on the server. - If a client-provided identifier is unavoidable, verify that it is explicitly associated with the authenticated principal. 3. **Use signed, short-lived charge intents** - Create charge intents server-side with a fixed skill ID and amount. - Bind each intent to the authenticated user, expected operation, expiration time, and unique nonce. - Verify the signature and all bound attributes before charging. 4. **Prevent replay and duplicate billing** - Generate a unique idempotency key for each intended skill invocation. - Persist consumed nonces or transaction identifiers and reject reuse. - Forward an idempotency key to SkillPay if its API supports one. 5. **Require explicit authorization where appropriate** - Obtain clear user confirmation before performing a charge. - Ensure that payment URLs and retry flows cannot silently produce duplicate charges. 6. **Apply abuse controls** - Rate-limit by authenticated user, source address, and account. - Add thresholds and alerts for repeated failures, unusual identifiers, and burst charge activity. - Return HTTP `401` for unauthenticated callers and `403` for unauthorized billing identities. 7. **Validate input and responses** - Enforce a strict request schema, body-size limit, and valid identifier format. - Check `response.ok` and validate the SkillPay response against an explicit runtime schema. - Avoid returning unnecessary upstream error details to untrusted callers. 8. **Preserve secret isolation** - Continue st ...[truncated 497 chars]
