Skip to content

critical: POST /transactions submits arbitrary third-party XDR with no source-account binding, idempotency, or rate limits #117

Description

@EmeditWeb

Problem

TransactionsService.submitTransaction() (src/modules/transactions/transactions.service.ts, lines 87–112) accepts any well-formed XDR string and immediately submits it to Horizon. It never checks that:

  1. Source account == authenticated wallet — the wallet parameter is used only to attribute the persisted record (line 101), not validated against transaction.source. A user can submit transactions signed by entirely different accounts, and the DB records them as that user's activity — poisoning loan/vendor dashboards, audit trails, and the indexer.
  2. No submission idempotency — the same XDR submitted twice hits Horizon twice (second may fail on seq, but the race is real for time-bounded txs), and two rows get inserted for one hash.
  3. No rate limiting — the endpoint is an open relay to Horizon; spamming costs the API's Horizon quota and lets abusers use the backend as a free transaction proxy.
  4. No operation allowlist — arbitrary operations (payments, account merge, set options, trustlines) pass through, including operations that make no sense in StepFi's flows.

Because persistTransactionRecord is fire-and-forget (line 101), Horizon succeeds even when persistence fails — the transaction hash is then unknown to the status-checker/indexer reconciliation paths.

Ground Rules

  1. Read context/architecture-context.md, context/code-standards.md, context/progress-tracker.md in full
  2. Read SubmitTransactionRequestDto and the transaction-type taxonomy it defines
  3. Read src/modules/transactions/transactions.repository.ts and the transactions table usage in getTransactionStatus() before changing persistence

What To Build

  1. Parse the XDR, extract the source account (and fee-bump inner source), and reject with a structured error unless it equals the authenticated wallet.
  2. Enforce an operation allowlist per declared type (e.g. loan_payment must contain the expected contract-invocation op) — reject mismatched payloads with codes like TRANSACTION_TYPE_MISMATCH.
  3. Idempotency: unique constraint on transaction hash + pre-insert existence check; duplicate submissions return the original record instead of re-submitting.
  4. Add per-wallet and global rate limits using the repo's established middleware patterns.
  5. Make persistence failures surface: retry queue or at minimum return 202-with-warning semantics documented in the DTO — do not silently drop.
  6. Tests covering every rejection branch plus the happy path regression.

Files To Touch

  • src/modules/transactions/transactions.service.ts
  • src/modules/transactions/dto/submit-transaction-request.dto.ts
  • src/modules/transactions/transactions.repository.ts
  • migration for unique hash constraint
  • tests
  • relevant docs/progress tracker

Acceptance Criteria

  • Third-party-sourced XDR is rejected with a typed error
  • Duplicate hashes cannot create duplicate rows or double submissions
  • Rate limits active per wallet
  • Suite green

Mandatory Checks Before Opening PR

Standard checklist applies. PRs failing any check will be closed without review.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions