Friday, July 18, 2025

The 3 Supabase Security Landmines That Will Wipe Your Startup's Data

Jeremy B

Supabase has democratized the backend, allowing teams to ship features faster than ever. But its power—connecting the client directly to a PostgreSQL database—comes with a critical security trade-off. You are now responsible for database-level access control, and a single, misconfigured SQL policy can expose every row of data you own.

This guide targets the three most common, yet catastrophic, security blunders that often plague fast-scaling Supabase projects.

1. The RLS Blind Spot: Why "Enabled" Isn't "Secure"

Row Level Security (RLS) is the single most important security feature in Supabase. It determines, row by row, what data a user (based on their JWT) can see and modify.

The failure here isn't usually forgetting RLS; it’s misconfiguring it.

The Two Critical RLS Mistakes:

A. The "Enable and Forget" Modification Policy

When you enable RLS on a table, it defaults to denying all access. You then create policies to grant access. A common and fatal mistake is only creating a SELECT policy and forgetting the policies for modification (INSERT, UPDATE, DELETE).

  • The Flaw: Developers often encounter an error when trying to INSERT and panic, creating an overly broad policy like: CREATE POLICY "Allow all authenticated users to insert" ON items FOR INSERT TO authenticated WITH CHECK (true).
  • The Catastrophe: If your RLS policy is simply WITH CHECK (true) for inserts, an authenticated user can insert data for any user, potentially creating fraudulent accounts or orders in another user's name.
  • The Fix: Use WITH CHECK for Inserts and Updates. For INSERT and UPDATE policies, the WITH CHECK clause must enforce that the new row they are creating or updating adheres to the required ownership rule.Here is the CORRECT INSERT POLICY you should use:CREATE POLICY "Users can create their own items" ON items FOR INSERT TO authenticated WITH CHECK (auth.uid() = user_id);

B. The Referenced Table Policy (The Hidden Deny)

If your RLS policy on Table A references (queries) data from Table B (e.g., "Only allow a user to update their posts if they are an admin in the roles table"), the RLS policy on Table B also applies to that internal check. If Table B is locked down with a too-strict policy, your policy on Table A will fail silently, or worse, deny access when it shouldn't.

  • The Fix: Ensure helper tables referenced by RLS policies have appropriate, tightly scoped SELECT policies for the authenticated role, allowing them to read only the necessary foreign data to resolve the policy check.

2. The Service Role Key Catastrophe

Supabase provides two primary API keys: anon (public) and service_role (secret). Misunderstanding their function is a direct path to a breach.

Key Distinction: anon vs. service_role

  • Key: anon Key (Public)
    • Purpose: Public, client-facing access (read-only for unauthenticated users).
    • RLS Status: Enforced.
    • Security Posture: Safe to expose if RLS is enabled.
  • Key: service_role Key (Secret)
    • Purpose: Administrative access for your secure backend systems.
    • RLS Status: Bypasses RLS.
    • Security Posture: NEVER expose to the client. Treat it like your database root password.

The Critical Key Blunder:

  • The Flaw: Using the service_role key in client-side code (in a React component, a mobile app, or a simple fetch request). This often happens when developers get frustrated debugging RLS and use the "master key" to make it work.
  • The Catastrophe: If an attacker extracts the service_role key from your client-side bundle (which is trivial), they gain full read/write access to your entire database, bypassing all your RLS policies. It is a total compromise.
  • The Fix: Only use the service_role key in secure, server-side environments: API routes (Next.js, Express), serverless functions (Supabase Edge Functions), or CI/CD pipelines. For the client, only use the anon key and rely solely on RLS for protection.

3. Exposed Postgres Functions and DDoS Risks

Supabase exposes database functions (Remote Procedure Calls or RPCs) over its REST API. Giving users broad execution rights on these functions creates a dangerous remote attack surface.

The Dangerous Function Permissions:

  • The Flaw: Giving the anon or authenticated roles EXECUTE permissions on PostgreSQL functions that perform privileged or network-capable operations.
  • The Catastrophe (SSRF): Functions using extensions like http (for making network requests) can be exploited to perform Server-Side Request Forgery (SSRF), allowing an attacker to probe your internal network.
  • The Fix (Principle of Least Privilege): Database functions that need high privilege should almost always be created with SECURITY DEFINER and only have EXECUTE rights granted to the service_role. If a normal user must execute it, the function must contain strict internal checks against auth.uid() and other security measures.

The Denial of Service (DDoS) Risk:

  • The Flaw: While RLS prevents unauthorized data access, it does nothing to prevent an authenticated user from spamming an authorized query thousands of times. Supabase does not offer per-user rate limiting out-of-the-box.
  • The Catastrophe: A malicious, but authorized, user can rapidly issue expensive queries, bringing your database to a halt and incurring huge compute costs.
  • The Fix: Offload all high-value, heavy queries to an Edge Function or API Route (like a Next.js API route) and implement granular rate limiting on that serverless endpoint using middleware. This is the only reliable way to prevent a single malicious user from exhausting your resources.