Skip to content

Latest commit

 

History

History
177 lines (136 loc) · 16.7 KB

File metadata and controls

177 lines (136 loc) · 16.7 KB

API Audit — three.ws

Date: 2026-06-18 · Scope: all 876 .js files under api/ (Vercel serverless functions) + the _lib helpers they import.

This audit was run subsystem-by-subsystem against the CLAUDE.md operating rules and standard API hardening criteria: no mocks/fake data, real APIs only, errors handled at boundaries, every path wired, auth + CSRF + rate-limit on the right routes, no secret/internal leakage, and the $THREE-only coin rule.

The codebase is, overall, mature and well-defended — SIWE/SIWS nonce handling, the x402 spec, on-chain payment verification, SSRF guards, cron-secret auth, and webhook signature checks are all correct. Findings cluster into a handful of systemic patterns rather than one-off bugs. Fixing the patterns at their shared seams (the wrap() boundary, the requireCsrf convention) clears most of the surface at once.

Severity summary

Severity Count Theme
CRITICAL 1 Fabricated sample avatars shipped into the live public discovery feed
HIGH ~30 (a) raw upstream err.message/body leaked in 5xx — leaks the Helius API key; (b) missing CSRF on cookie-session mutations; (c) payment-correctness races; (d) missing rate limits; (e) two dead/always-failing oracle routes; (f) SSRF gaps
MEDIUM ~25 retired model IDs, public PII leak, idempotency gaps, validation gaps
LOW ~20 response-shape inconsistencies, header omissions, hygiene

Coin policy: CLEAN. No reference to any coin/token/mint other than $THREE anywhere in the API. The only hardcoded mints are USDC and native/wrapped SOL (payment-rail plumbing, explicitly allowed) and the canonical $THREE CA.


Systemic patterns (fix these first — highest leverage)

P1 — Raw 5xx error leakage (security: leaks HELIUS_API_KEY)

wrap() (api/_lib/http.js:284) returns err.message verbatim for any uncaught status >= 500, and dozens of catch blocks call error(res, 500, code, e.message) directly. Solana/web3.js network errors embed the full RPC URL — https://mainnet.helius-rpc.com/?api-key=<HELIUS_API_KEY> — so a single upstream RPC failure spills the platform's Helius key to the client. Other leaks expose Alchemy/Bonfida/GraphQL/SDK internals.

Fix: route every status >= 500 through serverError() (which already sanitizes to a correlation ref + logs detail server-side). One change in wrap() covers all uncaught 5xx; the explicit-error() 5xx call sites that leak a keyed URL are fixed individually.

Worst offenders (keyed-URL leak): api/pump/[action].js:273,4651,4697, api/coin/[mint]/cohorts.js:113, api/tx/explain.js:48,83. Plus ~20 lower-sensitivity leaks across agents/, bazaar/, skills/, marketplace/, scene/, nft/, inference/.

P2 — Missing CSRF on cookie-session mutations

The platform convention (e.g. api/api-keys/[id].js, api/x/post.js:21) is: any state-changing request authed by the __Host- session cookie must pass requireCsrf. Bearer-token callers are auto-exempt. A cluster of mutating routes omit it:

Fix: add the one-line requireCsrf guard (session-authed branches only).

P3 — Missing rate limits where a dedicated limiter already exists


CRITICAL

# File Issue Fix
C1 api/_lib/demo-avatars.js → consumed by explore.js, explore-item.js, discover-detail.js A hardcoded fabricated DEMO_AVATARS array (invented authors/dates, generic glTF samples dressed as real community avatars) is injected into the live public discovery feed, counted into public totals, and served as detail/OG pages. Direct violation of the no-fake-data rule. Stop injecting the fixture into production responses; serve only real DB rows. Keep it strictly for empty-state local dev if at all.

HIGH (selected — full list tracked in fixes below)

# File:line Issue Fix
H1 api/x402-pay.js:715 + agent-trade-guards.js:409 Daily-cap race on agent pay path: SELECT SUM then settle, spend recorded fire-and-forget after settle → concurrent payments overspend the ceiling. Reserve-first in the ledger before signing (mirror x402-spending-cap.js).
H2 api/subscriptions/index.js:99 Subscription flipped to active with a full period before payment confirms; charge only inserts a pending intent. Free access. Grant the active period only after the tx confirms.
H3 api/x402/crypto-intel.js:166, api/x402/three-intel.js:156 On upstream failure a paying caller gets a 200 with fabricated signal:'neutral' and a false "estimated from trend memory" provenance; settle proceeds → charged for invented data. Throw err.status=502 on live-data failure so settle never runs; delete the false rationale.
H4 api/_lib/royalty.js:84 settleRoyalties redeems on-chain then UPDATEs unconditionally → concurrent runs double-pay. Claim rows atomically (UPDATE…SET status='settling' WHERE status='pending' RETURNING) before redeeming.
H5 api/agents/by-address/[addr].js:78 MAX_ENUM=50 defined but never applied — enumerates full balance into thousands of RPC calls. Slice to MAX_ENUM, set truncated.
H6 api/oracle/social.js:62 limits.moderate is undefinedrateLimited fires on every request; the ingest endpoint is permanently 429 (dead). Use a real limiter (limits.publicIp).
H7 api/oracle/follow.js:40 Per-branch method(...,['GET']) 405s every POST/DELETE → subscribe/unsubscribe are dead routes; readJson(req,res) disables the size cap. Branch on req.method; call method once with all verbs; fix readJson.
H8 api/_lib/vision.js:108 describeImage forwards caller imageUrl to the model server with no SSRF check; any non-forge consumer can reach internal targets. Validate scheme/host in imagePart().
H9 api/users/[username].js:272 Public endpoint returns every user's referral_code → referral-code harvesting. Drop referral_code (and re-check wallet_address) from the public payload.
H10 api/nft/mint-scene.js:14 Uses the retired nft.storage classic /upload API (dead upstream → mints fail); unbounded base64 blob into memory. Migrate to the storage provider used in avatars/[id]/[action].js; cap bytes.
H11 api/agents/payments/[action].js:498 Fund-adjacent distribute/withdraw prep+confirm have no rate limit and resolve agent by mint with no ownership gate. Add limits.authIp; assert agent.user_id === auth.userId.
H12 api/tx/explain.js:27, api/tx/solana/[action].js:54 No base58/hex validation on sig/mint before forwarding to keyed upstreams. Validate before fetch.

MEDIUM (selected)

# File Issue Fix
M1 api/agents/talk.js:26 ALLOWED_MODELS includes retired claude-3-5-haiku-20241022 (404s at provider). Replace with claude-haiku-4-5.
M2 api/brain/chat.js:102 Haiku 4.5 OpenRouter mirror points at retired anthropic/claude-3.5-haiku. Update to current Haiku 4.5 id.
M3 api/_lib/x402-spending-price.js:86 Cap under-counted via attacker-labeled USDC/inflated decimals in the 402 challenge. Derive decimals/classification from the on-chain mint / trusted registry.
M4 api/payments/solana/[action].js:118 confirmPayment UPDATE not conditional on status='pending'; no cross-intent tx replay guard (EVM path has one). Conditional UPDATE + unique (chain_type, tx_hash).
M5 api/agenc/[action].js:422 Top-level catch returns raw err.message in 500. Route through serverError.
M6 api/avatar/optimize.js:160 Expensive transcode, no auth/rate-limit; redirect not re-validated per hop. Add limiter; redirect:'manual' + re-validate.
M7 api/auth/github/[action].js:45 OAuth state not bound to a per-browser cookie → account-injection within the 10-min window. Bind state to a __Host- cookie.
M8 api/billing/withdrawals/index.js:14, api/billing/withdrawals/[id].js:18 currency_mint free-text, unvalidated; id not UUID-validated → 500 on bad input. Allow-list mints; validate UUID → 400.

LOW (themes)


Verified-clean (no action)

  • Coin policy$THREE only; USDC/SOL are payment plumbing.
  • No mocks/TODOs/stubs/fake-loading outside C1 — all setTimeout are real async (backoff/SSE keepalive/confirmation polling).
  • SIWE/SIWS, OAuth/PKCE, x402 spec, on-chain payment verification, withdrawals (advisory-lock + conditional insert), SSRF guards, cron-secret auth, webhook signatures, admin requireAdmin gating, parameterized SQL — all correct.

Remediation status (2026-06-19)

Shipped in this pass (verified: node --check + targeted vitest):

  • P1wrap() now sanitizes every uncaught 5xx to a correlation ref (no err.message in the body); the keyed-HELIUS_API_KEY leak sites in pump/[action].js (×6), coin/[mint]/cohorts.js, and tx/explain.js now route through respondError/fixed messages. A broad lower-priority sweep sanitized ~47 more >=500 err.message leaks across agents/, bazaar/, skills/, scene/, nft/, inference/, assets/, seed/.
  • C1DEMO_AVATARS fixture + generator deleted; all 8 consumers serve real DB rows only; obsolete OG demo test removed.
  • P2requireCsrf added to all 15 cookie-session mutations (bearer stays exempt). Handler-logic tests isolated via a requireCsrf mock; security-csrf-gates green.
  • P3 / H11 — rate limits wired on auth/wallets (+ Redis-backed single-use link nonces), auth/github, oauth/authorize, the three money-moving admin actions, and the four agents/payments fund-prep/confirm handlers (which also gained the ownership gate).
  • H3 — both paid intel endpoints now throw 503 on a live-data miss (verified the buyer is not charged: handler() runs before settlePayment).
  • H4settleRoyalties claims rows atomically (pending → settling, new migration 20260619000000_royalty_settling_status.sql) before the on-chain redeem; no double-pay.
  • H5agents/by-address enumeration capped at MAX_ENUM with a truncated flag.
  • H6 / H7oracle/social real limiter; oracle/follow POST/DELETE no longer dead (branch on req.method, readJson size cap restored).
  • H8vision.describeImage now applies a synchronous SSRF guard (https + block private/loopback IP literals + localhost) covering every consumer, with no live-DNS dependency.
  • H9users/[username] no longer leaks referral_code.
  • H12 / M4tx/explain validates sig before forwarding; Solana plan-payment confirm claims the intent atomically (conditional UPDATE → 409 on race).
  • M1 / M2 — retired Claude Haiku model ids replaced (talk.js, brain/chat.js).

Deferred — require a verified follow-up PR (do not ship blind on money paths):

  • H1 (agent spending-cap race) — the race-free reserve-first ledger (x402-spending-cap.js) is wired only into x402-user-payer.js. Bringing the agent pay path (x402-pay.js + agent-trade-guards.js) onto it means reserving in the ledger before signing and rolling back on settle failure — a change to the live signing path that needs a funded test wallet to validate. Remediation: reserve-then-sign-then-finalize with rollback, keyed on agent_id, mirroring x402-user-payer.js.
  • H2 (subscription active-before-pay)creator_subscriptions is granted an active period before payment, AND its confirmPayment activation function has zero callers (the in-app pay-confirm path is unwired). A one-line status flip would permanently lock out every subscriber. The duplicate-subscription race the audit also flagged is already prevented by the unique(plan_id, subscriber_user_id) constraint. Remediation (3 parts): (a) migration adding a pre-active status to the CHECK constraint; (b) wire confirmPayment into the tx-confirmation/cron path; (c) initialize new subs in the pre-active state. Until then the parallel subscription_checkouts/verify.js flow remains the correctly-gated path.

Pre-existing / concurrent-agent test failures (NOT from this pass — confirmed against clean HEAD): agent-monetization (2, env), ibm-attest (2, Guardian env), oauth-token (2, env), and widgets (4) — the last is a real CSRF-placement bug introduced by a concurrent commit (aaa038c1): on widgets/[id] PATCH the CSRF check fires before the bearer-scope check, so a scoped-bearer request gets csrf_missing instead of insufficient_scope. Fix: gate that route's CSRF with the if (session && …) form so bearer callers reach the scope check.

Fix plan (execution order)

  1. P1 boundary — sanitize wrap() 5xx + the keyed-URL leak sites. (security)
  2. C1 — remove DEMO_AVATARS from production responses. (rule violation)
  3. H6/H7 — repair the two dead oracle routes. (broken functionality)
  4. M1/M2 — retired model IDs. (broken functionality)
  5. H9 — stop leaking referral codes. (PII)
  6. P2 — CSRF cluster. (security)
  7. P3 + H11 — rate-limit gaps. (abuse)
  8. H3/H4/H2/H1/M4 — payment correctness. (money)
  9. H5/H8/H12/M6 — enumeration cap, SSRF, input validation.
  10. Remaining MEDIUM/LOW hygiene.

Each fix is verified against npm test and the existing helper conventions; no behavior is mocked, stubbed, or left half-wired.