This document describes the security invariants the app depends on. Any change to the codebase or Supabase configuration that violates these invariants must be reviewed carefully before merging.
Every table that holds user data has RLS enabled with a USING (auth.uid() = user_id) policy (see sql_scripts/supabase_rls_complete.sql).
This must never be disabled or weakened.
Verify at any time by running the diagnostic query at the bottom of supabase_rls_complete.sql in the Supabase SQL Editor.
Even though RLS enforces data isolation at the database level, every UPDATE and DELETE query in src/services/transactions.js and src/services/accounts.js also includes .eq("user_id", user.id).
This is defense-in-depth: if RLS is ever misconfigured, the application layer still prevents cross-user mutations.
Never remove .eq("user_id", user.id) from mutation queries without a documented reason.
React auto-escapes all JSX interpolations, preventing reflected XSS. The app has zero uses of dangerouslySetInnerHTML.
This must remain zero. Any PR introducing it requires explicit security sign-off.
signOut() in src/services/supabase.js calls clearAllOfflineData() before ending the Supabase session, ensuring no financial data persists in IndexedDB or sessionStorage after a user logs out.
Never call supabase.auth.signOut() directly; always go through the signOut() wrapper.
Every route under /app/* is wrapped in ProtectedRoute, which checks supabase.auth.getSession() before rendering. Public routes (/, /auth) do not have access to user data.
Any new page added under /app/ must use ProtectedRoute.
VITE_SUPABASE_ANON_KEY is the only Supabase credential baked into the frontend build. It is intentionally public (Supabase design). The service_role key must never appear in VITE_* variables or anywhere in the frontend codebase.
src/utils/csvParser.js rejects files larger than 10 MB before loading them into memory.
src/services/import.js rejects batches exceeding 5,000 rows.
These limits must not be raised without evaluating memory and denial-of-service risk.
| Threat | Mitigation |
|---|---|
| Horizontal privilege escalation (User A reads/writes User B's data) | RLS on all tables + user_id filter on all mutations |
| XSS → data exfiltration | No dangerouslySetInnerHTML; Content-Security-Policy header restricts script sources |
| Clickjacking | X-Frame-Options: DENY + CSP frame-ancestors 'none' |
| MIME sniffing | X-Content-Type-Options: nosniff |
| Sensitive data left in browser after logout | clearAllOfflineData() + sessionStorage.clear() on sign-out |
| Memory exhaustion via large file upload | 10 MB file size guard before arrayBuffer() |
| Credential leakage | Only anon key in bundle; service key never in frontend |
Run locally:
cd budget-app
npm run audit:securityThe GitHub Actions workflow at .github/workflows/security.yml runs npm audit --audit-level=high on every push, PR, and weekly on Mondays.
When npm audit reports a vulnerability:
- Check whether the vulnerable code path is reachable from the app.
- Update the package if a fix is available:
npm update <package>or pin viaoverridesinpackage.json. - If no fix exists, document the risk and set a review date.