T09 · Insecure Skill Coding Practices
- Location
src/mpp/tempo-client.ts:159- Finding
Arbitrary Network Origin and Untrusted Payment Recipient Enable Wallet Draining
- Content
View full analysis
, privateKeyHex: Hex ): Promise { const url = new URL(`${MOBULA_BASE_URL}${path}`); Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v)); // First attempt — no auth const res1 = await fetch(url.toString()); ``` The recipient supplied by the remote HTTP 402 response is then used directly in the signed transaction: ```ts const memo = challenge.memo ?? buildAttributionMemo(challenge.id, challenge.realm); let txHash: Hex; try { txHash = await walletClient.writeContract({ address: USDC_E, abi: USDC_ABI, functionName: "transferWithMemo", args: [challenge.recipient as Hex, BigInt(challenge.amount), memo], }); } catch (err: any) { throw new Error(`Tempo tx failed: ${err.message ?? err}`); } ``` The generic CLI command accepts the path without validating that it is a normalized Mobula API path: ```ts case "call": { const [path, ...kvs] = rest; if (!path) { console.error('Usage: bun run start call [key=value ...]'); console.error('Example: bun run start call /api/2/wallet/activity wallet=0xabc'); process.exit(1); } const params: Record = {}; for (const kv of kvs) { const idx = kv.indexOf("="); if (idx > 0) params[kv.slice(0, idx)] = kv.slice(idx + 1); } const w = await requireCliWallet(); const data = await tempoFetch(path, params, w.privateKey); console.log(JSON.stringify(data, null, 2)) ...[truncated 2910 chars]- Remediation
View remediation
