Skip to content

API hardening: trusted client IP, bounded bodies, limiter integrity, and no RPC-key leakage #111

Open

Description

@Ayush7614

Problem

The API has several independent resource-exhaustion and limit-bypass gaps. They are cheap to fix together because they share the same request helpers.

  1. Rate-limit identity is inconsistent. app/src/lib/profiles/http.ts:5 correctly uses fly-client-ip, then the last x-forwarded-for hop. Thirteen routes instead take the first XFF entry: bridge/quote, bridge/status, launch/chart-provider, launch/edit, launch/edit/nonce, launch/image, launch/meta, launch/sync, posts, posts/mod, posts/report, presence, and rpc. The first entry is whatever the caller sent, so a caller can rotate it to get a fresh bucket when the app is behind a proxy that does not set fly-client-ip (self-host, another platform, or a misconfigured deployment).

  2. The shared limiter wipes every bucket at 20,000 keys. app/src/lib/launchpad/editServer.ts:15-27 calls buckets.clear(), and expired buckets are never pruned. Once the process has seen 20k distinct keys, the next new key resets every active limit. The RPC token bucket has the same pattern at 10k (app/src/app/api/rpc/route.ts:37-52).

  3. Unauthenticated JSON endpoints buffer unbounded bodies. launch/edit, launch/edit/nonce, launch/meta, posts, posts/report, posts/mod and rpc call req.json() directly. Next route handlers have no body cap, so a chunked request can make the process buffer arbitrary bytes before validation. The multipart image route checks content-length only; a chunked upload bypasses that check.

  4. The sync response can leak the keyed RPC URL. pollAll() returns reason: errMessage(e) for a failed chain (indexer.ts:665); POST /api/launch/sync returns that object unauthenticated. Viem errors quote the upstream URL, and the paid RPC URL carries its API key in the path. The DB copy is redacted (indexer.ts:837), the response is not. The tx branch also returns raw err.message.

  5. The RPC proxy buffers unbounded request and response bodies. A batch can be up to 20 calls; the body and upstream response are read with req.json() / res.text() with no byte cap. eth_getLogs can return a very large response.

Impact

  • Per-IP limits can be bypassed or reset, so expensive endpoints (sync, bridge quote, chart, image, posts, RPC) can be driven harder than intended.
  • A single large body can exhaust server memory.
  • The RPC provider URL/API key can be disclosed to any caller who can make an upstream call fail.
  • The RPC proxy can be used to make the process buffer a huge response.

Proposed fix

  • Move the trusted-IP helper and body readers into app/src/lib/http.ts; keep profiles/http.ts as a re-export shim so existing imports keep working.
  • Use clientIp() in every route that currently parses XFF itself.
  • Replace the rate limiter's clear() with expired-first pruning + oldest-first eviction; do the same for the RPC token bucket.
  • Use readJson() for the object endpoints and a bounded readJsonValue() for the RPC batch; use bounded readFormData() for the image route.
  • Cap the RPC upstream response; reject over-cap responses with 502.
  • Redact sync errors in the route and in pollAll results; keep full details out of the unauthenticated response.

Acceptance criteria

  • No API route takes the first x-forwarded-for entry as the client identity.
  • An oversized declared or streamed body is rejected without buffering past the cap; a chunked oversized multipart upload is rejected too.
  • A limiter overflow cannot reset unrelated buckets; the RPC token bucket does not clear all state.
  • /api/launch/sync never returns a URL/API key from an RPC error.
  • Unit and source-contract tests cover the helpers and the route wiring; npm test, typecheck, lint, build pass.

Non-goals

  • No change to the existing per-route limits or quotas.
  • No change to signatures, admin auth, or DB schema.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions