Skip to content

fix(security): rate limit /api/stellar/submit and /api/stellar/sign-and-submit (closes #365) - #540

Open
Vyacheslav-Tomashevskiy wants to merge 1 commit into
Northgate-Systems:mainfrom
Vyacheslav-Tomashevskiy:fix/365-rate-limit-stellar-submit
Open

fix(security): rate limit /api/stellar/submit and /api/stellar/sign-and-submit (closes #365)#540
Vyacheslav-Tomashevskiy wants to merge 1 commit into
Northgate-Systems:mainfrom
Vyacheslav-Tomashevskiy:fix/365-rate-limit-stellar-submit

Conversation

@Vyacheslav-Tomashevskiy

Copy link
Copy Markdown
Contributor

Closes #365

The actual gap

/api/stellar/send already had rateLimit(\send:${user.id}`, 20, 60_000)`. The two endpoints that actually spend a transaction did not:

route before after
/api/stellar/send 20 / min / user unchanged (now sends Retry-After)
/api/stellar/submit none 20 / min / user
/api/stellar/sign-and-submit none 10 / min / user

So an authenticated session could loop submit without limit — each call is a Horizon round-trip plus two DB writes — and could spray sign-and-submit, which accepts a raw Stellar secret key and signs server-side. That one gets the tighter budget for exactly that reason: a stolen session cookie shouldn't buy unlimited signing attempts.

Changes

  • src/app/api/stellar/submit/route.ts, src/app/api/stellar/sign-and-submit/route.tsrateLimit() from @/lib/security keyed per user, placed after the auth check and before any body read, DB query or Horizon call, so a throttled request costs nothing. Each emits the existing logSecurityEvent("rate_limited", …) event, same as the routes that already had limits.
  • src/lib/api-response.tserrorResponse() takes an optional third headers argument (defaults to previous behaviour exactly, no existing call site changes shape). Used to attach Retry-After to the three 429s in src/app/api/stellar/. A 429 with no Retry-After gives a client nothing to back off on.
  • Limits are named constants at the top of each route rather than inline magic numbers.

I deliberately left the auth/* and analytics limiters alone — they already have limits, and consolidating all of them is #366's job, not this PR's.

Tests

14 new tests, all mocked, no network:

  • src/app/api/stellar/submit/__tests__/route.test.ts (7) — first 20 allowed, 21st is 429 in the standard {success:false,error} shape, Retry-After present and within the window, a throttled call reaches neither Horizon nor the DB, budgets are per-user (a noisy account can't lock out another), anonymous callers still get 401, and a request that fails schema validation still burns budget (otherwise malformed bodies are a free unlimited channel).
  • src/app/api/stellar/sign-and-submit/__tests__/route.test.ts (7) — the same matrix at the 10/min budget, plus: it is strictly tighter than submit, and a rejected secret key still burns budget, so key-guessing is throttled rather than free.

Negative control: reverting only the two route files and re-running these tests turns 10 of the 14 red; restoring them puts it back to 14/14. The tests fail without the fix.

npx eslint src/app/api/stellar src/lib/api-response.ts → clean, 0 warnings. npx tsc --noEmit → 0 errors in any touched file.

Two disclosures

  1. @/lib/validations is currently broken on mainReferenceError: isValidStellarPublicKey is not defined at src/lib/validations.ts:40, introduced by fix(send): validate the Stellar address checksum client-side (closes #424) #527 (the .refine() was added without importing the helper). It makes npm run build fail and 4 test files fail to collect; verified it reproduces on a clean origin/main via git stash. The fix is waiting in the still-open fix(validations): restore broken isValidStellarPublicKey import; feat: add /api/stellar/fee-estimate #529. Because submit/route.ts imports that module, the submit test mocks @/lib/validations with a byte-identical copy of stellarSubmitSchema (that schema is not affected by the bug) — commented in the file, and the mock can be deleted once fix(validations): restore broken isValidStellarPublicKey import; feat: add /api/stellar/fee-estimate #529 lands.
  2. Minor textual overlap with my open fix(security): enforce request body size limits on every JSON-body route (closes #379) #534, which switches these same two routes from request.json() to readBodyWithLimit(). Different lines (limiter goes above the body read), so they should merge in either order; happy to rebase whichever lands second.

/api/stellar/submit and /api/stellar/sign-and-submit had no rate limit
at all: an authenticated session could spray unlimited Horizon
round-trips and DB writes, and unlimited server-side signing attempts.
Both now use the existing rateLimit() helper, keyed per user, with
sign-and-submit held to a tighter budget because it accepts a raw
secret key. 429 responses now carry Retry-After (send included).

Closes Northgate-Systems#365
@vercel

vercel Bot commented Sep 9, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the codex723's projects Team on Vercel.

A member of the Team first needs to authorize it.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add rate limiting to /api/stellar/send and /api/stellar/submit

1 participant