The Policy Nobody Wrote: Reading a Supabase Table Without Row Level Security

Web & API Security Level 3/4 ~5 min November 8, 2026

The challenge

A profile page asks Supabase for two columns. Ask for every column of every row instead. Bring back the admin's API key.

What you'll learn

  • Read a PostgREST URL: table, select list, filters
  • Explain why the Supabase anon key is public and what actually protects the data
  • Widen a query from two columns of one row to the whole table
  • Tell a user-chosen display name from a server-side role
  • Lock a Supabase table down with RLS, a public view and column grants

Skills tested

API testingBroken access controlRequest tampering

Prerequisites

  • What a query string is
  • What a database table and its columns are

How it works

Supabase puts a REST API (PostgREST) in front of Postgres. The browser talks to it directly: the URL names the table (/rest/v1/profiles), the columns (select=display_name,avatar_url) and the rows (id=eq.<uuid>). Every request carries the project's anon key, a JWT with "role":"anon" that ships in the app's JavaScript.

That key is public by design. What protects the data is Row Level Security: policies written in Postgres that decide, per role, which rows a query may see. With RLS disabled on a table the anon role can select, the URL is the only limit, and the browser writes the URL. The page asking for two columns of one row is a convention, not a control.

In 2025, researchers found that apps generated with Lovable, an AI app builder, often shipped tables with RLS off (CVE-2025-48757): anyone with the public key could list users, emails and stored API keys.

Common mistakes

  • Stopping at select=*. With the id=eq. filter still there you get every column of Sam's row only, and Sam is a member. Delete the filter to see the other rows.
  • Taking the row called Admin. display_name is whatever a user typed at sign-up. That row has "role":"member"; the admin is the row with "role":"admin".
  • Answering Sam's key. It is the first key you see, but it belongs to the profile on screen.
  • Forging a service_role token. Editing the JWT breaks its HS256 signature, and PostgREST answers PGRST301. No forgery is needed: the anon key already reads everything.
  • Removing the apikey header. Supabase's gateway refuses the request before PostgREST sees it.

How to defend against it

  • Enable RLS on every table in an exposed schema: alter table profiles enable row level security; Supabase's Security Advisor flags tables where it is off.
  • Write explicit policies, for example owners read their own row: create policy "own row" on profiles for select to authenticated using (auth.uid() = id);
  • Serve public fields through a view or a function that returns only display_name and avatar_url, or revoke column privileges from anon.
  • Keep secrets out of browser-readable tables: store API keys hashed, in a schema the Data API does not expose, and rotate any key that was ever readable.
  • Never decide privilege from user-editable fields such as a display name.

Full solution

Pro and Max members unlock the complete step-by-step walkthrough.

Go Pro

Community stats

132 completions
71% success rate
M2F14M3 First blood

Related Daily Hacks

38,000+ Hackers Real labs Free
Start Hacking Free or solve today's hack, no account needed