Skip to content

fix: resolve security and correctness issues (#1099 #1100 #1101 #1102)#1124

Merged
ogazboiz merged 1 commit into
LabsCrypt:mainfrom
leojay-net:fix/1099-1100-1101-1102-security-auth-migration-routes
Jun 27, 2026
Merged

fix: resolve security and correctness issues (#1099 #1100 #1101 #1102)#1124
ogazboiz merged 1 commit into
LabsCrypt:mainfrom
leojay-net:fix/1099-1100-1101-1102-security-auth-migration-routes

Conversation

@leojay-net

Copy link
Copy Markdown
Contributor

Summary

This PR addresses four issues filed against the backend:

#1102 — Remove duplicate mark-defaulted route registration

loanRoutes.ts registered POST /:loanId/mark-defaulted twice inside the same NODE_ENV guard (lines 62–69 and 72–79). Express only dispatches to the first matching handler, so the second block was unreachable dead code and a maintenance hazard.

Change: Removed the duplicate registration block.


#1101 — Add missing requireScopes('read:score') to /:userId/breakdown

Every sibling score route (GET /:userId, /:walletAddress/history, /:walletAddress/nft) enforces requireScopes('read:score') between requireJwtAuth and the param check. The breakdown route was missing it, allowing any JWT—regardless of scopes—to read score breakdown data.

Changes:

  • Added requireScopes('read:score') to the GET /:userId/breakdown route in scoreRoutes.ts
  • Added a test in scoreBreakdown.test.ts asserting that a token lacking read:score receives 403

#1100 — Replace !== with crypto.timingSafeEqual in requireApiKey

auth.ts compared API key secrets with k.secret !== keyStr inside Array.find. String inequality short-circuits on the first differing byte, leaking per-character timing information that can be exploited to recover INTERNAL_API_KEY over many requests.

Changes:

  • Added import crypto from "node:crypto"
  • Replaced the secret equality check with crypto.timingSafeEqual on fixed-length Buffer instances (length checked first; keys of different lengths are rejected immediately without calling timingSafeEqual)
  • Added a unit test in apiKeyScopes.test.ts verifying a wrong key of identical length is rejected

#1099 — Fix ensure-core-tables migration colliding with the loan_events view

Migration 1788000000018_unified-contract-events.js renames the loan_events table to contract_events and creates a view named loan_events. The subsequent migration 1789000000000_ensure-core-tables.js guarded its CREATE TABLE loan_events with IF NOT EXISTS (SELECT FROM pg_tables WHERE tablename='loan_events'). pg_tables only enumerates ordinary tables (relkind r/p) and excludes views, so the guard evaluated TRUE and attempted to create the table, failing with relation loan_events already exists on any clean sequential migration run.

Changes:

  • Replaced the pg_tables guard with to_regclass('public.loan_events') IS NULL, which resolves any relation type including views
  • Added an explanatory comment cross-referencing the earlier migration
  • Added a migration-check job in .github/workflows/ci.yml that spins up a fresh Postgres 16 service and runs npm run migrate:up against it end-to-end

Test plan

  • backend CI job: lint, build, typecheck, and existing test suite pass
  • migration-check CI job: all migrations apply cleanly against a fresh empty database
  • New test: GET /api/score/:userId/breakdown with a scopeless token → 403
  • New test: requireApiKey with a wrong key of the same byte-length as the configured key → throws (rejected)

Closes #1099
Closes #1100
Closes #1101
Closes #1102

…#1100 LabsCrypt#1101 LabsCrypt#1102

- (LabsCrypt#1102) Remove duplicate POST /:loanId/mark-defaulted route registration
  in loanRoutes.ts; Express only ever dispatches to the first handler so
  the second block was unreachable dead code.

- (LabsCrypt#1101) Add requireScopes('read:score') to the GET /:userId/breakdown
  route in scoreRoutes.ts, matching the guard applied to every sibling
  score route. Adds a test asserting a token without that scope gets 403.

- (LabsCrypt#1100) Replace string inequality (k.secret !== keyStr) in requireApiKey
  with crypto.timingSafeEqual to prevent per-byte timing side-channels on
  the INTERNAL_API_KEY. Length is checked first; timingSafeEqual is used
  only on equal-length buffers. Adds a unit test for wrong-key-same-length.

- (LabsCrypt#1099) Fix ensure-core-tables migration: replace the pg_tables guard
  (which only covers ordinary tables) with to_regclass(), which resolves
  views too. Prevents the migration from attempting to CREATE TABLE
  loan_events when the unified-contract-events migration has already
  created a VIEW of that name. Adds a migration-check CI job that runs the
  full migration set against a fresh Postgres 16 instance.
@ogazboiz ogazboiz merged commit 7ae6d80 into LabsCrypt:main Jun 27, 2026
5 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment