T09 · Insecure Skill Coding Practices
- Location
SKILL.md:149- Finding
Unvalidated Server-Controlled Payment Terms Are Signed by the Client Wallet
- Content
View full analysis
Vulnerability Details
File Location:
SKILL.md, lines 149–165
Vulnerability Type: Untrusted payment authorization parameters
Risk Level: HighVulnerable Code
python # Step 3: sign EIP-3009 accepts = payment_req["accepts"][0] # pick Base or Solana signature = await wallet_client.sign_eip3009( amount=int(accepts["maxAmountRequired"]), to=accepts["paymentInfo"]["payTo"], usdc_contract=accepts["paymentInfo"]["extra"]["erc20Address"], valid_before=int(time.time()) + 300, nonce=secrets.token_hex(16), ) # Step 4: encode and retry payment_header = base64.b64encode(json.dumps({ "scheme": accepts["scheme"], "networkId": accepts["networkId"], "payload": signature, }).encode()).decode()Technical Analysis
The example decodes a
PAYMENT-REQUIREDresponse and signs the first entry in the server-controlledacceptsarray without independently validating:- The exact expected payment amount or a caller-approved maximum.
- The recipient against VeraData's documented wallet addresses.
- The USDC contract against an allowlist for the selected chain.
- The network identifier against the wallet's intended network.
- The payment scheme and decoded response structure.
- Whether the selected acceptance option corresponds to the requested endpoint and published price.
Base64 provides encoding only; it does not provide authenticity, confidentiality, or integrity. Consequently, encoding the signed payload does not mitigate the trust-boundary problem.
Although the initial request uses a fixed HTTPS origin, a compromised API, TLS interception environment, DNS compromise, or malicious replacement service at that origin could return attacker-selected payment parameters. A wallet implementation that signs the supplied values literally could then authorize a transfer that differs from the payment the user intended.
This exceeds least privilege for the declar ...[truncated 2250 chars]
- Remediation
View remediation
Remediation Suggestions
- Maintain a local allowlist of supported network IDs, canonical USDC contracts, payment schemes, and authorized recipient wallets.
- Define a local endpoint-to-price policy and reject any amount above the exact expected price or an explicit user-approved ceiling.
- Do not automatically select
accepts[0]. Filter all options through policy and fail closed if no approved option remains. - Bind the authorization to the intended endpoint, request, chain, asset, recipient, and amount where the protocol supports such binding.
- Require explicit user confirmation before signing when any payment term differs from previously approved policy.
- Implement the same validation inside
wallet_clientso callers cannot bypass it by supplying arbitrary values. - Validate the decoded JSON against a strict schema, including types, allowed fields, amount bounds, expiry bounds, and nonce requirements.
- Reject redirects and unexpected origins, enforce TLS certificate verification, and apply appropriate connection and response-size limits.
- Check both HTTP responses and payment-verification results before treating a transaction as successful.
- Document that base64 is transport encoding rather than a security control, and display the human-readable payment terms before authorization.
A hardened flow should resemble:
python APPROVED = { "eip155:8453": { "scheme": "exact", "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "pay_to": "0xCf1d31020A7915421f6d66B9835Dcb6f422337E7", } } expected_amount = endpoint_price_policy[endpoint] approved = [] for option in payment_req.get("accepts", []): network = option.get("networkId") policy = APPROVED.get(network) payment_info = option.get("paymentInfo", {}) extra = payment_info.get("extra", {}) if ( policy and option.get("scheme") == policy["scheme"] and payment ...[truncated 474 chars]
