T09 · Insecure Skill Coding Practices
- Location
skills/pay-per-call/run.ts:795- Finding
Unvalidated Server-Supplied Poll URL Enables SSRF and a Wallet Signing Oracle
- Content
View full analysis
| { error: string }> { const owner = keypair.publicKey(); let res: Response; try { res = await fetch(`${pollUrl}/challenge`, { headers: { "X-Stellar-Owner": owner }, }); } catch (err) { return { error: `challenge request failed — ${(err as Error).message}` }; } if (!res.ok) { let detail = ""; try { detail = ((await res.json()) as any)?.error ?? ""; } catch { /* non-JSON body */ } return { error: `challenge ${res.status}${detail ? ` — ${detail}` : ""}` }; } let body: { nonce?: string }; try { body = (await res.json()) as { nonce?: string }; } catch { return { error: "challenge response was not JSON" }; } if (!body.nonce) return { error: "challenge response had no nonce" }; const message = new TextEncoder().encode( `${OWNERSHIP_DOMAIN}:${jobId}:${body.nonce}`, ); if (message.length === 32) { return { error: "refusing to sign a 32-byte payload" }; } const signature = keypair.sign(Buffer.from(message)).toString("base64"); return { "X-Stellar-Owner": owner, "X-Stellar-Nonce": body.nonce, "X-Stellar-Signature": signature, }; } ``` ```ts res = await fetch(pollUrl, { headers: authHeaders }); ``` ```ts if (res.status === 202) { const pollUrl = res.headers.get("x-job-poll-url"); const jobId = res.headers.get("x-job-id"); if (pollUrl) { console.error(`⏳ Async job started (id=${jobId ?? "unknown"})`); console.error(` Poll URL: ${pollUrl}`); const result = await pollJobStatus( pollUrl, new URL(pollUrl).pathname.split("/").pop() ?? "", Keypair.fromSecret(signe ...[truncated 2588 chars]- Remediation
View remediation
