T09 · Insecure Skill Coding Practices
- Location
scripts/billing.py:54- Finding
Non-idempotent billing retries can cause duplicate charges
- Content
View full analysis
BillingResult: # SkillPay API v1 format headers = { "X-API-Key": self.api_key, "Content-Type": "application/json", } # Use configured price (in USDT) payload = { "user_id": user_ref, "skill_id": self.skill_id, "amount": float(self.price_usdt), # USDT per call } delay = 0.8 for attempt in range(3): try: resp = self.session.post(f"{self.billing_url}/charge", json=payload, headers=headers, timeout=12) ``` The caller generates an idempotency key, but it is not included in the request: ```python def maybe_bill(call_name: str) -> Dict[str, Any]: client = build_billing_client() amount = os.getenv("SKILLPAY_PRICE_USDT", "0.002").strip() user_ref = os.getenv("SKILLPAY_USER_REF", "anonymous").strip() or "anonymous" result = client.charge(call_name, amount, user_ref, str(uuid4())) ``` ### Technical Analysis The billing client accepts an `idempotency_key` and retries the charge operation up to three times, but never transmits the key to the billing provider. Charging is a state-changing financial operation. If the provider applies a charge but the response is lost, times out, or returns an HTTP 5xx status, the client cannot distinguish that condition from a request that was never processed. The retry loop may therefore submit the same logical charge several times. Because each request lacks a stable server-recognized idempotency identifier, the billing provider may treat every retry as a separate transaction. ### Attack Path 1. A user invokes a paid `scan` or `audit` command. 2. `maybe_bill()` creates an idempotency key and calls the billing client. 3. The billing pro ...[truncated 983 chars]- Remediation
View remediation
