Supabase rls audit
@shipcheck/supabase-rls-audit
Audit row level security in your Supabase migrations before you launch. Finds tables with RLS off, policies that let anyone write, policies that trust user_metadata, views that skip RLS, and risky SECURITY DEFINER functions, then drafts a fix migration for you to review. Reads .sql files only and never touches your database.
- Activity
- New: be the first to try it
- Installs
- 0 installs
- Likes
- 0 likes
- Clones
- 0 clones
- 1---
- 2name: supabase-rls-audit
- 3description: Audit Supabase row level security (RLS) from the migration files in supabase/migrations. Finds tables with RLS off, policies with USING (true) or WITH CHECK (true), policies that only check "is signed in", policies that trust user_metadata, views and SECURITY DEFINER functions that skip RLS, then drafts a fix migration for review. Use before launch, after adding tables, when someone asks "is my Supabase secure", "check my RLS", "can users see each other's data", or after a Supabase security advisor warning.
- 4license: MIT
- 5---
- 7# Supabase RLS audit
- 9In Supabase, your anon key ships to every browser. Row level security is the only thing between that key and your tables. This skill reads your migrations, replays them in order, and reports what the final schema lets strangers and other users do. It never connects to your database.
- 11## When to use
- 13- Before launch, or before sharing the app with real users.
- 14- After adding a table, a view, or a policy.
- 15- "Is my Supabase locked down?", "can user A read user B's rows?", "why does the security advisor complain?"
- 16- Not for: Storage bucket policies or Edge Function auth. Say so and offer a manual review instead.
- 18## Inputs
- 20- The migrations folder, default `supabase/migrations`. If the project has none (common with dashboard-only projects), ask the user to run `supabase db pull` themselves, or to paste the schema into `supabase/schema.sql`, then point the script at a folder holding that file.
- 21- Which tables are meant to be public (blog posts, pricing, a public profile). Ask once, after the first run, only if there are `policy-public-read` findings.
- 23## Steps
- 251. **Run the audit.**
- 27 ```bash
- 28 node scripts/rls-audit.mjs supabase/migrations --json out/rls-audit.json --md out/rls-audit.md
- 29 ```
- 312. **Read `out/rls-audit.md`** and show the user the summary line and every high finding, in plain words: who can do what to which table. Example: "Anyone with your anon key can read and change every row in `invoices`."
- 323. **Check each finding against the code.** Open the migration it names. Confirm the owner column (often `user_id`, `owner_id`, or `id` on profile tables). Drop a finding only if you can say why it is safe, and say so in the report.
- 334. **Ask about public tables.** For each `policy-public-read`, ask: "Is `<table>` meant to be readable by anyone on the internet?" Keep it if yes.
- 345. **Draft a fix migration** at `out/rls-fixes.sql` (not in `supabase/migrations` yet). One commented block per finding. Use `(select auth.uid())` rather than bare `auth.uid()`: same result, much faster on big tables.
- 356. **Show the draft and ask** before copying it into `supabase/migrations/<timestamp>_rls_fixes.sql`. Tell the user to test it locally first, then apply it with their usual deploy. Re-run step 1 on the folder to confirm the high count is 0.
- 37## Fix patterns
- 39```sql
- 40-- Owner-only table
- 41alter table public.notes enable row level security;
- 42create policy "notes: owner reads" on public.notes for select to authenticated
- 43 using ((select auth.uid()) = user_id);
- 44create policy "notes: owner writes" on public.notes for insert to authenticated
- 45 with check ((select auth.uid()) = user_id);
- 46create policy "notes: owner updates" on public.notes for update to authenticated
- 47 using ((select auth.uid()) = user_id) with check ((select auth.uid()) = user_id);
- 49-- View that should respect RLS
- 50alter view public.note_counts set (security_invoker = true);
- 52-- Admin check: app_metadata, which only the server can set
- 53using ((auth.jwt() -> 'app_metadata' ->> 'role') = 'admin')
- 54```
- 56## Output format
- 58```markdown
- 59# RLS audit: <project>, <date>
- 60High 3 | Medium 4 | Low 0 | Info 1
- 62## Fix now
- 63- invoices: RLS is off. Anyone can read and change every invoice. Fix in rls-fixes.sql block 1.
- 65## Decide
- 66- profiles is readable by anyone. You said this is intended: kept.
- 68## Fine as is
- 69- audit_log: RLS on, no policies, so only your server can reach it.
- 70```
- 72## Rules for findings
- 74- **High:** RLS off on a table in an exposed schema; a write policy with `true`; a write policy that only checks the user is signed in; any policy reading `user_metadata`.
- 75- **Medium:** public read with `USING (true)`; open insert; views without `security_invoker`; SECURITY DEFINER functions without `set search_path`.
- 76- **Info:** RLS on with no policies (server-only, usually fine).
- 78## Guardrails
- 80- Never run SQL against a database, local or remote, and never run `supabase db push` or `db reset`. The user applies migrations.
- 81- Never edit an existing migration file. Fixes go in a new migration.
- 82- Never put the service role key in client code as a "fix". If the app breaks after RLS is on, the fix is a policy, not a bigger key.
- 83- The script parses common SQL. If a migration uses dynamic SQL or `DO` blocks to create tables or policies, say the audit may miss them and review those by hand.
