| ← All Docs Security

Security

Verified Last updated: September 2026

Security overview covering authentication, authorization, Row Level Security, and known vulnerabilities.

Authentication

Dealer Authentication

Staff Authentication

Admin Authentication

Row Level Security (RLS)

RLS is enabled on 22+ tables in Supabase PostgreSQL. All dealer-facing tables enforce tenant isolation.

Policy Patterns

PatternUsageApplied To
dealer_id = auth.uid()Dealer sees only their own dataBills, farmers, inventory, payments, expenses, etc.
staff_dealer_id()Staff sees their dealer's dataSame tables, via a helper function that resolves staff → dealer
SECURITY DEFINERRPCs that bypass RLSAdmin RPCs, cross-tenant operations, aggregation queries
SECURITY DEFINER functions run with the permissions of the function owner (typically postgres), bypassing RLS. This is intentional for admin operations but must be carefully audited.

Known Security Issues

10 known security issues ranked by severity. Each includes a description, impact, and recommended fix.

Critical 1. Service Role Key Committed to Repository

The Supabase service role key is present in scripts/migrate/.env.migration and was committed to the git repository. This key has full database access, bypassing all RLS policies. Update: .env.migration is now listed in .gitignore (line 43), so new commits no longer include it. However, the key remains in git history and is still exposed to anyone who can clone the repo.

Impact: Anyone with repo access can read/write/delete all data in the production database by checking out a historical commit.

Recommendation: Rotate the service role key immediately. Scrub git history with git filter-branch or BFG Repo-Cleaner to remove the key from all prior commits. Use environment variables or a secrets manager for deployment scripts.

Critical 2. farmer_return_credit_applications Table Missing RLS

The farmer_return_credit_applications table has no RLS policies enabled. Any authenticated user can read and write all rows regardless of dealer.

Impact: Cross-tenant data exposure. A dealer can see and modify another dealer's return credit applications.

Recommendation: Enable RLS on the table and add the standard dealer_id = auth.uid() policy for SELECT, INSERT, UPDATE, and DELETE.

High 3. Admin RPCs Callable by Anon Role

Functions prefixed with admin_* (e.g., admin_get_dealer_profile, admin_resolve_ticket) are callable by the anon Supabase role. No authentication is required.

Impact: Unauthenticated users can access admin functionality, view dealer profiles, resolve support tickets, and potentially modify data.

Recommendation: Revoke EXECUTE permission on all admin_* functions from the anon role. Add role checks inside each function as defense-in-depth.

High 4. Storage Buckets Publicly Readable

The farmer-profiles and product-images storage buckets are configured with public read access. Anyone with the URL can access these files.

Impact: Farmer profile photos and product images are exposed without authentication. Enumerable if bucket listing is enabled.

Recommendation: Make buckets private. Serve images through authenticated URLs or signed URLs with expiry. If public access is intentional, document it and ensure no sensitive data is stored in these buckets.

High 5. pin_hash Leaked in Admin API Response

The admin_get_dealer_profile RPC returns the pin_hash field in its response. Even though it's bcrypt-hashed, exposing password hashes is a security anti-pattern.

Impact: Offline brute-force attacks against staff PINs. PINs are short (4–6 digits), making them feasible to crack even with bcrypt.

Recommendation: Exclude pin_hash from the RPC response. Never return password/PIN hashes to any client.

Medium 6. Staff Permissions Enforced Client-Side Only

Staff permission checks (e.g., "can create bills", "can view reports") were originally enforced only in the React frontend. Update: The staff_has_permission() helper is now used in RLS policies for cashbook and expenses, enforcing those permissions server-side. Other modules (bills, reports, inventory, etc.) still rely on client-side checks only.

Impact: For cashbook and expenses, API-level bypass is no longer possible. For all other modules, a staff member who knows the API can still bypass UI restrictions and perform any action their dealer account can perform.

Recommendation: Extend staff_has_permission() RLS enforcement to the remaining modules (bills, reports, inventory, farmers, etc.).

Medium 7. No Admin Login Rate Limiting

The admin login endpoint has no rate limiting or account lockout. Unlike staff PIN entry (which is rate-limited), admin passwords can be brute-forced without restriction.

Impact: Brute-force attacks against admin accounts.

Recommendation: Add rate limiting (e.g., 5 attempts per minute) and temporary account lockout after repeated failures.

Medium 8. CORS Wildcard on WhatsApp Edge Function

The send-bill-whatsapp edge function sets Access-Control-Allow-Origin: *, allowing any website to call it.

Impact: Third-party websites could trigger WhatsApp messages on behalf of dealers if they obtain a valid auth token.

Recommendation: Restrict CORS to the application's domain(s) only.

Low 9. No CSP Headers Configured

The application does not set Content-Security-Policy headers. This increases exposure to XSS attacks.

Impact: If an XSS vulnerability is found, there's no CSP to limit the damage.

Recommendation: Configure CSP headers in the hosting platform (Vercel/Netlify headers config) with a policy that restricts script sources, frame ancestors, and form actions.

Low 10. No Audit Log for Permission Changes

Audit logging exists for some operations (e.g., subscription changes, bill edits) but does not cover staff permission changes, role assignments, or admin setting modifications. These security-relevant operations have no audit trail.

Impact: Cannot investigate who changed staff permissions or role assignments, or when, if a security incident occurs.

Recommendation: Extend the existing audit log to record staff permission changes, role assignments, and admin actions with timestamps and actor IDs.