Summary
Five separate security findings across the messaging encryption + audit logging stack. None of these is a known incident, but each is a latent risk with high blast radius.
Findings
1. Private ECDH key in plaintext localStorage (HIGH)
src/services/messaging/key-service.ts:54-70 — exports private key to JWK and stores in localStorage. Any XSS anywhere in the app compromises all messages and defeats the E2E zero-knowledge guarantee. The comment acknowledges this was for Playwright support.
Fix: Wrap the localStorage cache with app-level symmetric encryption (derived from a PIN or session-token-hash), OR scope the cache to test environments via build flag.
2. JWT prefix logged on every send (HIGH)
src/services/messaging/message-service.ts:151-157 — debug log includes tokenPrefix: session?.access_token?.slice(0, 20) on every send-message attempt. Visible to DevTools, browser extensions, log shippers.
Fix: Replace with opaque hasToken: !!session?.access_token.
3. Audit-logger has no credential filter (HIGH)
src/services/auth/audit-logger.ts:28 — event_data: Record<string, any> has no exclusion of password/token/secret/key fields. One accidental caller passing { password } lands a plaintext credential in the database.
Fix: Strip credential-named keys in the log() method before insert.
4. Client-only rate limiter (HIGH)
src/lib/auth/rate-limiter.ts — stores attempt counts in localStorage. Incognito mode or localStorage.clear() resets all counters. This is security theatre — any attacker bypasses it trivially.
Fix: Move rate limiting to server side (Supabase Edge Function or Postgres trigger on auth_audit_logs). Keep client-side as UX hint only; document that it's not a security control.
5. Dead crypto code with latent length bug (HIGH)
src/lib/key-derivation.ts:290-348 — createPKCS8() is unreachable but contains a hardcoded length byte (0x41 = 65) that's only correct for 32-byte keys. If re-enabled by a future contributor, produces silently broken keys.
Fix: Delete the method.
6. verifyPublicKey null-safety hole (HIGH)
src/lib/messaging/key-derivation.ts:170-188 — JWK x/y comparison: if stored key has missing y field (possible from older schema), comparison silently returns false. User locked out of messaging permanently.
Fix: Add explicit checks for both x AND y on both keys before comparing.
Why this matters
Six items across crypto + audit + rate-limiting; none is a known incident, but each is a latent breach or lockout risk.
Related
- Code review findings: CR-024, CR-029, CR-030, CR-031, CR-032, CR-033
Summary
Five separate security findings across the messaging encryption + audit logging stack. None of these is a known incident, but each is a latent risk with high blast radius.
Findings
1. Private ECDH key in plaintext localStorage (HIGH)
src/services/messaging/key-service.ts:54-70— exports private key to JWK and stores in localStorage. Any XSS anywhere in the app compromises all messages and defeats the E2E zero-knowledge guarantee. The comment acknowledges this was for Playwright support.Fix: Wrap the localStorage cache with app-level symmetric encryption (derived from a PIN or session-token-hash), OR scope the cache to test environments via build flag.
2. JWT prefix logged on every send (HIGH)
src/services/messaging/message-service.ts:151-157— debug log includestokenPrefix: session?.access_token?.slice(0, 20)on every send-message attempt. Visible to DevTools, browser extensions, log shippers.Fix: Replace with opaque
hasToken: !!session?.access_token.3. Audit-logger has no credential filter (HIGH)
src/services/auth/audit-logger.ts:28—event_data: Record<string, any>has no exclusion of password/token/secret/key fields. One accidental caller passing{ password }lands a plaintext credential in the database.Fix: Strip credential-named keys in the
log()method before insert.4. Client-only rate limiter (HIGH)
src/lib/auth/rate-limiter.ts— stores attempt counts in localStorage. Incognito mode orlocalStorage.clear()resets all counters. This is security theatre — any attacker bypasses it trivially.Fix: Move rate limiting to server side (Supabase Edge Function or Postgres trigger on
auth_audit_logs). Keep client-side as UX hint only; document that it's not a security control.5. Dead crypto code with latent length bug (HIGH)
src/lib/key-derivation.ts:290-348—createPKCS8()is unreachable but contains a hardcoded length byte (0x41 = 65) that's only correct for 32-byte keys. If re-enabled by a future contributor, produces silently broken keys.Fix: Delete the method.
6. verifyPublicKey null-safety hole (HIGH)
src/lib/messaging/key-derivation.ts:170-188— JWK x/y comparison: if stored key has missingyfield (possible from older schema), comparison silently returns false. User locked out of messaging permanently.Fix: Add explicit checks for both x AND y on both keys before comparing.
Why this matters
Six items across crypto + audit + rate-limiting; none is a known incident, but each is a latent breach or lockout risk.
Related