Skip to content

hard: ApiKeyGuard hammers Supabase per request, leaks timing behavior, and has no per-key abuse controls #121

Description

@EmeditWeb

Problem

ApiKeyGuard.canActivate() (src/auth/guards/api-key.guard.ts, lines 35–114) on EVERY vendor request:

  1. Computes sha256(key) and runs .select('*').eq('key_hash', hash).single() against Supabase — two DB round trips per request counting the last_used_at UPDATE (which fires unconditionally, line 104–113). Under vendor traffic this is pure hot-path database load with zero caching, making the guard itself the scalability ceiling and a denial-of-service amplifier (flooding random keys saturates the DB connection pool).
  2. Performs a direct hash-equality DB match — fine cryptographically — but the surrounding error behavior distinguishes API_KEY_INVALID vs API_KEY_INACTIVE vs API_KEY_EXPIRED (lines 59–80), letting an attacker enumerate whether a captured key hash corresponds to a revoked vs expired vs nonexistent key. Low severity individually; sloppy combined with the next point.
  3. Has no per-key rate limiting, lockout, or anomaly tracking: a leaked key can be driven at line rate indefinitely. permissions array check (lines 87–95) uses .some(includes) — acceptable — but there is no wildcard support or permission hierarchy, pushing consumers toward over-permissioned keys.
  4. Fires the updateLastUsed write on every request with no throttling — write amplification scales 1:1 with reads.

Ground Rules

  1. Read context/architecture-context.md, context/code-standards.md, context/progress-tracker.md in full
  2. Read the cache-manager usage patterns already established in src/modules/liquidity/liquidity.service.ts and src/modules/transactions/transactions.service.ts
  3. Keep the request.apiKey contract intact for controllers

What To Build

  1. Cache key records by hash prefix (never store full keys in cache) with a short TTL; invalidate on revocation via the vendors service write path.
  2. Collapse last_used_at writes to at-most-once-per-N-minutes-per-key (cache-guarded dirty flag).
  3. Normalize all failure responses to a single API_KEY_UNAUTHORIZED code (log details server-side only).
  4. Add per-key sliding-window rate limits with structured 429 responses, wired through the repo's established middleware/guard patterns.
  5. Tests: cache hit avoids DB call (mock assertions); revocation invalidates within one TTL; rate limit trips and resets; enumeration uniformity of error codes.

Files To Touch

  • src/auth/guards/api-key.guard.ts
  • src/modules/vendors/vendors.service.ts (cache invalidation hook)
  • tests
  • relevant docs/progress tracker

Acceptance Criteria

  • Steady-state vendor traffic causes ≤ 1 key lookup per TTL per key
  • Write amplification bounded
  • Rate limiting active per key
  • 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