Cenk Kurtoğlu©26

Case study (redacted) · 6 minute read

Inside a redacted Supabase security review: the anon role read every customer email

Most people never see what a security review deliverable looks like before they buy one, which makes it impossible to judge the value. This is an anonymized walkthrough of a completed fixed-price review: the project details, table names and customer data are removed, but the finding, the evidence, the repair and the report structure are exactly as they were delivered.

The project

A small B2B product built in a weekend with an AI coding assistant: Next.js front end, Supabase for auth and data, launched to its first customers a week later. The owner had asked the AI to “make the database secure” and the dashboard showed RLS enabled, so the app shipped. No one had tested what an unauthenticated visitor could actually read.

The finding (HIGH-1)

The central table holding customer records had RLS enabled but a policy that applied to every role, with a SELECT grant still in place for anon. The Supabase REST endpoint therefore answered anonymous requests with the full contents of the table — every customer row, including email addresses and order records — without a login or any key beyond the public anon key that ships in every Supabase JavaScript bundle by design.

The evidence, exactly as delivered

The report did not assert the finding; it reproduced it. Each finding carried a request/response pair captured read-only, the scope of the exposure, and the minimal SQL that both proves and closes it:

# evidence — anonymous request against the REST endpoint
curl -s "$SUPABASE_URL/rest/v1/customers?select=*" \
  -H "apikey: $ANON_KEY"

HTTP/1.1 200 OK
[
  { "id": 1, "email": "[redacted]@example.com", "order_ref": "[redacted]" },
  { "id": 2, "email": "[redacted]@example.com", "order_ref": "[redacted]" },
  ...
]

# minimal proof from the SQL editor
select relname, relrowsecurity
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relname = 'customers';   -- relrowsecurity = true

select policyname, roles, cmd, qual
from pg_policies
where schemaname = 'public' and tablename = 'customers'; -- roles = {public}, permissive

The report also stated plainly what a documents-only review could not verify — whether the policy as written actually blocks a cross-tenant read at runtime — and listed that as a separate runtime-evidence item for the staging harness, so nobody mistook a static reading for a tested guarantee.

The repair, as one coherent approach

The report recommended a single repair approach rather than a menu of options, so the owner and any implementer could act without reconciling competing suggestions: enable row level security with a user-scoped policy, remove the anon grant, keep the service-role key server-side only, and verify with a two-user staging harness that the anon request now returns an empty array rather than data. Each step came with the SQL to apply and the check that proves the step worked.

alter table public.customers enable row level security;

create policy "customers: own row only"
  on public.customers
  for select
  to authenticated
  using (auth.uid() = user_id);

revoke select on public.customers from anon;

What the deliverable contained

The final report was a prioritized list of findings, each with its own identity (so later discussions could reference “HIGH-2” without ambiguity), the evidence capture, the impact in plain language, the exact repair, and a runtime-evidence checklist. It ended with an implementer brief: the minimum another developer needs to apply the fixes without the reviewer in the room, plus the five-business-day clarification round for anything the owner's own adviser questioned.

What made this different from a scanner report

The automated scan the owner had already run flagged “RLS enabled: yes” and stopped. The review connected three facts a scanner reports separately — a permissive policy applying to anon, a surviving SELECT grant, and an exposed endpoint — and showed the combination returning real data. Tools are used deliberately in the review (secret-pattern scanning, policy checks, endpoint probes), but every finding is read and verified by hand before it enters a report, and no client material is ever sent to a third-party AI service or subcontractor.

Check your own app before launch

Supabase RLS Audit Kit · $29 one-time

Run the same categories of checks yourself: commented SQL audits, cross-user and cross-tenant isolation tests, plus report and fix-SQL templates shaped like this deliverable.

Use it only on projects you own or are authorized to test. It is a self-run toolkit, not a managed penetration test or security certification.

Get the kit - $29 ↗Review the files, scope and license →

Have a real review done instead

If your app is live, holds customer data, or is about to launch, a fixed-price Supabase RLS audit from $99 returns this same structure on your schema: evidence-backed findings ranked by severity, one coherent repair approach, and the checks that prove the fixes closed the exposure. Typical delivery is 48 hours from receiving the materials.

Template bundle

Review all 20 frontend starters in one bundle

20 editable frontend starters with live demos are $79 — one payment, no subscription.

View the Bundle — $79↗