Back to Prompts
Security10 min read

The Supabase Triple Threat: How Three Small Mistakes Create One Big Security Breach

In Supabase, your 'API' is actually PostgREST sitting on PostgreSQL. Learn how IDOR + data exposure + broken auth chain together for devastating attacks, and get our audit prompt to find these vulnerabilities.

Stuffnthings Security Team
April 1, 2026
Share:
The Supabase Triple Threat: How Three Small Mistakes Create One Big Security Breach

# The Supabase Triple Threat

In traditional web development, you build an API layer in Node.js, Python, or Go that sits between your frontend and database. You write authentication middleware, validation logic, and business rules in application code.

**But in Supabase, your "API" is actually a thin layer (PostgREST) sitting directly on top of your PostgreSQL database.** This means your security logic isn't in a Node.js middleware; it's inside the database itself.

This architectural difference creates a unique attack surface. Here's how the **"Triple Threat" Chain** looks and breaks in a Supabase environment, and how you fix it.

## 1. The IDOR Gotcha (Enumeration)

**The Scenario:** You have a table `projects` with an auto-incrementing integer `id`.

* **The Hole:** Even with Row Level Security (RLS) enabled, if your policy is slightly off—or if you accidentally set a table to "Public"—an attacker can guess `id=101`, `id=102`, etc.
* **The Supabase Fix:** **Use UUIDs.**
* When creating tables, set the `id` column to type `uuid` with the default `gen_random_uuid()`.
* Integers are predictable; UUIDs are practically impossible to guess.

## 2. Excessive Data Exposure (The PostgREST Leak)

**The Scenario:** You call `supabase.from('projects').select('*')`.

* **The Hole:** By default, `select('*')` returns **every column** in that table. If you have a column like `internal_admin_note` or `project_secret_key`, it gets sent to the browser.
* **The Supabase Fix:** **Database Views.**
1. Create a view that only includes safe columns
2. **Revoke** all permissions on the raw table
3. **Grant** select permission only on the view

## 3. The "Triple Threat" Chain in Supabase

This is how a real hack would happen on a Supabase stack by chaining minor mistakes:

1. **The Chain Link 1 (IDOR):** You used integer IDs. The attacker finds your project at `id=50`. They guess `id=51`.
2. **The Chain Link 2 (Data Leak):** Your RLS policy allows public reads. The attacker fetches `id=51` and gets a hidden `creator_id` UUID.
3. **The Chain Link 3 (Broken Auth):** You created a Database Function to "Reset User Settings" but forgot to check if `auth.uid() == input_id`.

**The Kill:** The attacker calls your function via `rpc('reset_settings', { input_id: 'leaked-uuid-from-step-2' })`. They just wiped another user's configuration.

## The Audit Prompt

Use this prompt with your AI coding assistant to systematically audit your Supabase application for these vulnerabilities.

Ready to Audit Your System?

Copy this prompt and use it with your AI coding assistant to find vulnerabilities.

**Context:** I am hardening my Supabase application against advanced attack chains. In Supabase, the API (PostgREST) is a direct reflection of the database schema. I need to ensure that a single vulnerability (like a leaky RLS policy) cannot be chained with others (like IDOR or excessive data exposure) to compromise the system.

**Task:** Audit my SQL schema, RLS policies, and Database Functions for the following "Triple Threat" vulnerabilities:

**1. IDOR & Enumeration Check:**
* Identify any tables using `BigInt` or `Serial` (integer) primary keys instead of `UUID`.
* For existing integer keys, suggest a migration to `UUID` or a strategy to mask them using a `hashid` or a Public View.

**2. Excessive Data Exposure (PostgREST Scrubbing):**
* Scan all tables for sensitive columns (e.g., `email`, `stripe_id`, `internal_notes`, `role`, `is_admin`).
* For these tables, write the SQL to create a **Secure View** using `WITH (security_invoker = true)` that excludes these sensitive fields.
* Provide the `REVOKE` and `GRANT` commands to ensure the `anon` and `authenticated` roles can only access the **View**, not the raw table.

**3. RLS Policy & Logic Audit:**
* Flag any RLS policies using `USING (true)` or `CHECK (true)`.
* Verify that every policy involving a user check uses the cached pattern: `((select auth.uid()) = user_id)` for performance and accuracy.
* Check all **Database Functions (RPCs)**:
* Ensure they are defined with `SECURITY INVOKER` by default.
* If `SECURITY DEFINER` is required, verify that `SET search_path = public` is present and that `auth.uid()` is manually checked inside the function body to prevent privilege escalation.

**4. Schema Hygiene:**
* Confirm all tables in the `public` schema have RLS enabled.
* Check for any "Shadow Admin" functions that bypass RLS but are exposed to the `authenticated` role.

**Output Requirement:** Provide a summary of found "holes" and the exact SQL migrations needed to fix them.