Skip to content

POST /api/simulate is an unauthenticated Tenderly proxy: anyone can burn the API key's quota #822

Description

@nanaf6203-bit

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

  1. 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.
  2. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Medium ComplexitybugSomething isn't working correctlysecuritySecurity issue or hardening opportunity

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions