Skip to content

fix(backend): compare requested wallet to authenticated wallet - #1451

Open
Armolas wants to merge 4 commits into
LabsCrypt:mainfrom
Armolas:fix/wallet-ownership-self-compare
Open

fix(backend): compare requested wallet to authenticated wallet#1451
Armolas wants to merge 4 commits into
LabsCrypt:mainfrom
Armolas:fix/wallet-ownership-self-compare

Conversation

@Armolas

@Armolas Armolas commented Jul 27, 2026

Copy link
Copy Markdown

Summary

Fixes #1363: requireWalletOwnership in backend/src/middleware/jwtAuth.ts compared the requested wallet to itself (requestedWallet !== requestedWallet), which is always false, so the ownership check never rejected a request. Any authenticated wallet could access another wallet's scoped resources, including GET /indexer/events/borrower/:borrower and GET /loans/borrower/:borrower.

  • Changed the comparison to requestedWallet !== authenticatedWallet, matching the pattern already used correctly by requireWalletParamMatchesJwt.
  • Added test coverage for requireWalletOwnership in backend/src/middleware/__tests__/jwtAuth.test.ts (previously untested), covering the params.borrower / params.wallet / body.wallet sources, the 403 mismatch case, 401 unauthenticated, and 400 missing wallet.

Pull Request Checklist

Please ensure your PR follows these steps, mirroring our CONTRIBUTING.md guidelines.

  • I have read the CONTRIBUTING.md document.
  • My code follows the code style of this project.
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • I have updated the documentation accordingly.
  • I have verified the changes locally.

Test plan

  • npm test -- src/middleware/__tests__/jwtAuth.test.ts in backend/ — all 6 new requireWalletOwnership tests pass.
  • Confirmed (by stashing the fix) that 2 pre-existing, unrelated requireScopes test failures predate this change and are untouched by it.

Armolas added 4 commits July 27, 2026 08:48
…uireWalletOwnership

requireWalletOwnership compared requestedWallet to itself instead of to
the authenticated wallet, so the check always passed and any authenticated
wallet could access another wallet's scoped resources (borrower loans/events).

Fixes LabsCrypt#1363
The keyset-pagination merge (adminDisputeController, indexerController,
remittanceController, lib/pagination) had never actually been run through
lint/build/test locally:

- lib/pagination.ts: decodeCursor/parseKeysetParams passed a plain
  { code: string } object where AppError.badRequest expects an ErrorCode
  enum member, failing typecheck/build.
- adminDisputeController/indexerController/remittanceController:
  req.query.snapshot_seq (string | ParsedQs | ...) was passed straight into
  parseKeysetParams without narrowing to string | null like the sibling
  cursor/limit params already did.
- indexerController's getBorrowerEvents/getLoanEvents had an inconsistent
  return (TS7030 not-all-paths-return); added the missing return.
- indexerController still imported cacheService, buildKeysetClause, and
  defined buildEventsCacheKey, none of which are used anywhere — removed.
- remittanceController's getRemittances ran a redundant, unused parallel
  COUNT(*) query (countResult was never read) alongside the real,
  snapshot-correct total-count query a few lines below — dropped it.
- 127 Prettier formatting errors across these files — ran lint:fix.

Also rewrote adminDisputePagination.test.ts and the two indexer-events
cases in paginationFiltering.test.ts, which were still asserting the old
offset/page_info response shape (page_info, flat data, raw-string cursor)
from before the keyset-pagination migration landed on these endpoints. The
controllers now correctly implement docs/pagination-contract.md's
page.{next_cursor,snapshot_seq,total_at_snapshot,limit} + data.items shape
with opaque base64url cursors; the tests were updated to match, not the
other way around.
Merged upstream/main (77 commits) and cherry-picked the earlier
keyset-pagination CI fix; on top of that, this branch had picked up further
breakage from very recent upstream commits:

- services/defaultChecker.ts: an orphaned, syntactically invalid duplicate
  of fetchOverdueLoanIds sat outside the DefaultChecker class (stray
  'private' keyword at module scope). Its corrected due_ledger < $2
  predicate (vs. the real method's <=, which flagged loans as overdue one
  ledger early) was folded into the real class method, and the dead
  duplicate was deleted.
- services/sorobanService.ts: removed an orphaned, broken
  updateUserScoresBulk that referenced a nonexistent this.userRepository;
  the real, used implementation already lives in services/scoresService.ts.
- middleware/pauseGuard.ts + errors/{AppError,errorCodes}.ts: pauseGuard.ts
  called AppError.serviceUnavailable(), which didn't exist — added it
  (503) following the existing factory-method/ErrorCode pattern. Also fixed
  a generic argument on the non-generic query() call and prefixed unused
  req/res middleware params.
- app.ts / routes/statusRoutes.ts: dropped an unused initializePauseState
  import from app.ts (it's already correctly called at startup in
  index.ts) and unused Request/Response/NextFunction type imports.
- services/authService.ts: verifyChallengeTimestamp rejected any
  future-dated challenge timestamp, but its own test suite (added in a
  later commit than the implementation) expects a small clock-skew
  tolerance for near-future timestamps. Added a 5s tolerance constant.
- middleware/auth.ts + assorted test files: pure Prettier formatting from
  the newest upstream commits, fixed via lint:fix.
- frontend PauseBanner.tsx: Prettier formatting fix.

Verified locally: backend lint/build/typecheck clean, 514/514 tests pass;
frontend lint/typecheck clean, 124/124 tests pass, build succeeds;
contracts fmt/clippy clean, 287/287 tests pass (untouched).
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.

[Backend] Wallet-ownership check compares a value to itself and always passes

1 participant