POST /api/simulate is an unauthenticated Tenderly proxy: anyone can burn the API key's quota
Labels / Complexity: bug · security · Medium Complexity — Medium
Problem
src/app/api/simulate/route.ts proxies arbitrary transaction-simulation requests to Tenderly with no authentication and no rate limiting (it does not use the withRateLimit helper that csp-report and errors routes use). Every request also forwards save: true and save_if_fails: true to Tenderly, so each call persists a simulation. An attacker — or a bot scraping the site — can POST unlimited simulations and:
- Exhaust the Tenderly API quota and incur costs on the project tied to the server-side
TENDERLY_API_KEY.
- Use the endpoint as a free oracle for transaction simulation (the service exists to simulate arbitrary
to/input payloads, which the site cannot distinguish from legitimate checkout traffic).
The route also echoes the full upstream errorBody back to the client (line ~52), disclosing Tenderly response internals.
Root cause
src/app/api/simulate/route.ts: no auth check, no rate limiting, and unconditional save: true/save_if_fails: true forwarding, with the server-side API key held in the route's env config.
Why this is architecturally hard
- Legitimate use needs to stay fast. Checkout-time simulation is the feature; adding auth/rate limiting must not break the wallet-driven flow that calls it. The design question is the gate: a CSRF-protected session check (the app has
src/lib/csrf.ts), a rate limit per session/IP (the app has src/lib/rateLimit.ts — the route just doesn't use it), or both.
- The
save flags are a cost lever. Whether simulations must persist (save: true) is a product decision; turning it off (or off by default) is the cheap mitigation, but it changes what the simulation history shows.
Acceptance criteria
POST /api/simulate rejects unauthenticated requests (or is rate-limited per session/IP via withRateLimit).
- The upstream
errorBody is no longer echoed verbatim to the client (logged server-side instead).
- A test covers an unauthenticated/over-limit request being rejected and a legitimate simulation still succeeding.
npm run typecheck, npm test, and npm run lint pass.
Out of scope
Choosing a different simulation provider is out of scope.
Getting started
src/app/api/simulate/route.ts — the route to gate
src/lib/rateLimit.ts — the existing helper the route does not use
src/lib/csrf.ts — the CSRF helper for session-bound gating
Commands: npm run typecheck, npm test, npm run lint.
Good first files to read: src/app/api/simulate/route.ts, src/app/api/csp-report/route.ts (the rate-limited sibling).
POST /api/simulate is an unauthenticated Tenderly proxy: anyone can burn the API key's quota
Labels / Complexity: bug · security · Medium Complexity — Medium
Problem
src/app/api/simulate/route.tsproxies arbitrary transaction-simulation requests to Tenderly with no authentication and no rate limiting (it does not use thewithRateLimithelper thatcsp-reportanderrorsroutes use). Every request also forwardssave: trueandsave_if_fails: trueto Tenderly, so each call persists a simulation. An attacker — or a bot scraping the site — can POST unlimited simulations and:TENDERLY_API_KEY.to/inputpayloads, which the site cannot distinguish from legitimate checkout traffic).The route also echoes the full upstream
errorBodyback to the client (line ~52), disclosing Tenderly response internals.Root cause
src/app/api/simulate/route.ts: no auth check, no rate limiting, and unconditionalsave: true/save_if_fails: trueforwarding, with the server-side API key held in the route's env config.Why this is architecturally hard
src/lib/csrf.ts), a rate limit per session/IP (the app hassrc/lib/rateLimit.ts— the route just doesn't use it), or both.saveflags are a cost lever. Whether simulations must persist (save: true) is a product decision; turning it off (or off by default) is the cheap mitigation, but it changes what the simulation history shows.Acceptance criteria
POST /api/simulaterejects unauthenticated requests (or is rate-limited per session/IP viawithRateLimit).errorBodyis no longer echoed verbatim to the client (logged server-side instead).npm run typecheck,npm test, andnpm run lintpass.Out of scope
Choosing a different simulation provider is out of scope.
Getting started
src/app/api/simulate/route.ts— the route to gatesrc/lib/rateLimit.ts— the existing helper the route does not usesrc/lib/csrf.ts— the CSRF helper for session-bound gatingCommands:
npm run typecheck,npm test,npm run lint.Good first files to read:
src/app/api/simulate/route.ts,src/app/api/csp-report/route.ts(the rate-limited sibling).