Supabase Security Audit (Bug Bounty)
Full security verification of a target's Supabase stack. Vibe-coded and many production apps share the same failure modes: RLS off, open policies, JWT metadata for roles, open Storage, and SECURITY DEFINER RPCs.
Authorization gate
Use only on targets with written authorization (bug bounty program, pentest SOW, or owned project).
Confirm in-scope assets (domains, project refs, APIs) before any active test.
Prefer non-destructive checks. Do not mass-exfiltrate PII; minimize evidence to PoC rows/fields.
If a service_role key is found, treat as Critical secret exposure — do not abuse it beyond program-allowed proof unless rules explicitly allow.
When to run
- User runs
/supabase-security
- Target uses Supabase (
.supabase.co, createClient, NEXT_PUBLIC_SUPABASE_*, PostgREST /rest/v1)
- Need RLS / anon-key / Storage / RPC review for bug bounty or hardening
Inputs to collect
Ask only for what is missing:
| Input |
Why |
| Target URL / app URL |
JS recon, key harvesting |
| Program scope / auth proof |
Legal boundary |
Supabase project URL (https://<ref>.supabase.co) |
API surface |
anon public key (from frontend) |
Authenticated-as-anon tests |
Optional: SQL Editor / psql access |
Full catalog audit (when authorized & available) |
| Optional: two test accounts |
IDOR / horizontal privilege checks |
Pipeline
0 Auth gate → 1 Recon → 2 Client surface → 3 Catalog SQL (if possible)
→ 4 Abuse validation → 5 Severity + report
Do not skip 0–2. Phase 3 requires DB access (Dashboard SQL Editor or psql); if unavailable, deepen 2 and 4 from the outside.
Phase 0 — Authorization
- Confirm target is in scope and testing is authorized.
- Note program rules on automated scanning, account creation, and credential use.
- If authorization is unclear, stop and ask the user.
Phase 1 — Recon
Map how the app talks to Supabase.
- Frontend / mobile bundle
- Search for:
supabase.co, SUPABASE_URL, SUPABASE_ANON_KEY, service_role, eyJ JWTs, createClient(.
- Extract project ref, anon key, any leaked service_role or DB URLs.
- OpenAPI / PostgREST
GET {SUPABASE_URL}/rest/v1/ with headers:
apikey: <anon>
Authorization: Bearer <anon>
- Note exposed tables, views, RPC names if schema is open or errors leak names.
- Auth methods in UI: email/password, magic link, OAuth, phone, anonymous sign-in.
- Storage / Realtime / Edge usage in network tab or source.
- Produce a short attack-surface note: project ref, keys found (redact in reports as needed), tables/RPCs suspected, auth model.
Phase 2 — Client-side surface (anon / user JWT)
All tests use only public credentials (anon key + attacker-controlled accounts) unless rules say otherwise.
2A. REST data access
For each suspected table T:
GET /rest/v1/T?select=*&limit=5
apikey: <anon>
Authorization: Bearer <anon_or_user_jwt>
Check:
| Check |
Red flag |
| Anon SELECT returns rows |
Public data leak / missing RLS |
| Authenticated user reads other users' rows |
IDOR / weak USING clause |
| INSERT/UPDATE/DELETE as anon |
Anon write |
INSERT as user A with user_id of B |
Missing WITH CHECK |
| PATCH role/is_admin/plan columns |
Mass assignment / broken column grants |
Prefer Prefer: count=exact and small limit — enough for PoC, not dumps.
2B. RPC / functions
POST /rest/v1/rpc/<fn>
Probe discovered function names. SECURITY DEFINER without auth checks often = privilege escalation.
2C. Auth / JWT
- Sign up two accounts; compare access to the same resources.
- If policies use roles from JWT: try
updateUser({ data: { role: 'admin' } }) (user_metadata is user-writable).
- Confirm whether admin/premium flags live in
user_metadata (Critical) vs app_metadata / server table.
2D. Storage
GET /storage/v1/bucket
GET /storage/v1/object/list/<bucket>
- Public buckets with sensitive objects
- Upload to buckets as anon/authenticated when not intended
- Path traversal / predictable object names / IDOR on object paths
2E. Realtime
- Subscribe to sensitive tables/channels as anon or low-priv user.
- Presence of unrestricted channels for private data.
2F. Edge Functions
- Call
/functions/v1/<name> with anon key.
- Missing JWT verification, SSRF, secrets in responses, IDOR in function logic.
2G. Secrets in client
service_role in JS/mobile → Critical.
- Database password / connection string in client → Critical.
- Long-lived user tokens in logs → High/Medium depending on impact.
Phase 3 — Catalog SQL audit (when DB access exists)
Read-only. Do not run DDL/DML "fixes" during bounty unless asked for remediation on owned projects.
| Access |
Script |
| Supabase SQL Editor |
references/audit-dashboard.sql (no \echo) |
psql |
references/audit-psql.sql |
| Deep pass / PT-BR red flags |
references/audit-complete.sql |
Run sections in order. Capture scorecard + every CRITICAL/WARN row.
Interpret with references/misconfig-catalog.md.
Phase 4 — Abuse validation
For each candidate finding, prove impact with a minimal PoC:
- Precondition (keys, account state)
- Request(s) (HTTP or SQL) — redact secrets in final writeup if required
- Observed result (status, sample fields, row counts — not full dumps)
- Impact (confidentiality / integrity / privilege)
- False-positive check (public-by-design marketing data? documented public bucket?)
Prioritize:
- service_role leak, RLS off on tenant tables, USING(true) on private data
- user_metadata authorization, anon write, SECURITY DEFINER without checks
- Storage IDOR, missing WITH CHECK, horizontal IDOR
- Info disclosure (schema, enums, error messages), missing FORCE RLS
Phase 5 — Report
Write supabase-security-report-YYYY-MM-DD.md using references/report-template.md.
Severity guide (adjust to program CVSS):
| Severity |
Examples |
| Critical |
service_role in client; RLS off + PII/tenant data; USING(true) private tables; user_metadata → admin |
| High |
Anon write on sensitive tables; DEFINER RPC priv-esc; Storage full read of private files |
| Medium |
IDOR on non-critical objects; missing WITH CHECK; app_metadata misuse if settable |
| Low |
Verbose errors; enum/role name leak; RLS perf anti-patterns |
| Info |
Hardening notes (FORCE RLS, indexes, Security Advisor) |
Each finding: title, severity, asset, evidence, PoC, impact, remediation, references (Supabase docs / OWASP).
Common vibe-coder failure modes (hunt these first)
- RLS never enabled on
public tables exposed via PostgREST
CREATE POLICY ... USING (true) "so the app works"
- Authorization via
auth.jwt() -> user_metadata / raw_user_meta_data
- No WITH CHECK on INSERT/UPDATE (owner spoofing)
- Anon policies for write left from prototypes
- SECURITY DEFINER RPCs granted to
anon/authenticated without auth.uid() checks or pinned search_path
- Storage buckets public with user uploads
- service_role in Next.js
NEXT_PUBLIC_* or mobile apps
- Views without
security_invoker bypassing RLS
- Realtime on tables that REST already protects poorly
Full catalog: references/misconfig-catalog.md.
Remediation patterns (for owned projects or report "fix")
-- Enable + force RLS
ALTER TABLE public.t ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.t FORCE ROW LEVEL SECURITY;
-- Owner-only pattern
CREATE POLICY t_select_own ON public.t
FOR SELECT TO authenticated
USING (user_id = (SELECT auth.uid()));
CREATE POLICY t_insert_own ON public.t
FOR INSERT TO authenticated
WITH CHECK (user_id = (SELECT auth.uid()));
- Roles: table
user_roles with its own RLS, or app_metadata set only by service_role / Auth Hook — never user_metadata for authz.
- DEFINER functions:
SET search_path = public, pg_temp, explicit authz, minimal GRANT.
- Re-run Supabase Dashboard → Database → Security Advisor after fixes.
Resource files
| Path |
Use |
references/audit-dashboard.sql |
Catalog audit in SQL Editor |
references/audit-psql.sql |
Catalog audit via psql (\echo sections) |
references/audit-complete.sql |
Extended PT-BR checklist + executive summary query |
references/misconfig-catalog.md |
Red flags, severity, PoC hints |
references/report-template.md |
Report skeleton |
Constraints
- Read-only preference; no destructive tests without explicit user request.
- Minimize data access; PoC-sized evidence only.
- Do not commit real keys, JWTs, or PII into git.
- Coordinate with
ethical-redteam / vuln-discovery when the engagement is broader than Supabase.
1---2name: supabase-security3description: Audits Supabase security on authorized bug bounty and pentest targets, focusing on misconfigurations common in vibe-coded and production apps: missing/broken RLS, USING(true)/WITH CHECK(true), JWT user_metadata privilege escalation, anon write policies, SECURITY DEFINER RPCs, storage buckets, exposed service_role keys, and PostgREST surface. Guides running the bundled SQL audit scripts, interpreting red flags, validating client-side exposure, and writing severity-ranked findings with PoC steps and remediations. Use when the user runs /supabase-security, or asks to audit Supabase, check RLS, review Supabase policies, hunt Supabase misconfigs, test anon key access, or secure a Supabase backend on a bug bounty target. Requires written authorization before any active testing.4license: MIT5---67# Supabase Security Audit (Bug Bounty)89Full security verification of a target's Supabase stack. Vibe-coded and many production apps share the same failure modes: RLS off, open policies, JWT metadata for roles, open Storage, and SECURITY DEFINER RPCs.1011> **Authorization gate**12> Use only on targets with written authorization (bug bounty program, pentest SOW, or owned project).13> Confirm in-scope assets (domains, project refs, APIs) before any active test.14> Prefer non-destructive checks. Do not mass-exfiltrate PII; minimize evidence to PoC rows/fields.15> If a `service_role` key is found, treat as Critical secret exposure — do **not** abuse it beyond program-allowed proof unless rules explicitly allow.1617## When to run1819- User runs `/supabase-security`20- Target uses Supabase (`.supabase.co`, `createClient`, `NEXT_PUBLIC_SUPABASE_*`, PostgREST `/rest/v1`)21- Need RLS / anon-key / Storage / RPC review for bug bounty or hardening2223## Inputs to collect2425Ask only for what is missing:2627| Input | Why |28|-------|-----|29| Target URL / app URL | JS recon, key harvesting |30| Program scope / auth proof | Legal boundary |31| Supabase project URL (`https://<ref>.supabase.co`) | API surface |32| `anon` public key (from frontend) | Authenticated-as-anon tests |33| Optional: SQL Editor / `psql` access | Full catalog audit (when authorized & available) |34| Optional: two test accounts | IDOR / horizontal privilege checks |3536## Pipeline3738```390 Auth gate → 1 Recon → 2 Client surface → 3 Catalog SQL (if possible)40→ 4 Abuse validation → 5 Severity + report41```4243Do not skip 0–2. Phase 3 requires DB access (Dashboard SQL Editor or `psql`); if unavailable, deepen 2 and 4 from the outside.4445---4647## Phase 0 — Authorization48491. Confirm target is in scope and testing is authorized.502. Note program rules on automated scanning, account creation, and credential use.513. If authorization is unclear, stop and ask the user.5253---5455## Phase 1 — Recon5657Map how the app talks to Supabase.58591. **Frontend / mobile bundle**60 - Search for: `supabase.co`, `SUPABASE_URL`, `SUPABASE_ANON_KEY`, `service_role`, `eyJ` JWTs, `createClient(`.61 - Extract project ref, anon key, any leaked service_role or DB URLs.622. **OpenAPI / PostgREST**63 - `GET {SUPABASE_URL}/rest/v1/` with headers:64 - `apikey: <anon>`65 - `Authorization: Bearer <anon>`66 - Note exposed tables, views, RPC names if schema is open or errors leak names.673. **Auth methods in UI**: email/password, magic link, OAuth, phone, anonymous sign-in.684. **Storage / Realtime / Edge** usage in network tab or source.695. Produce a short attack-surface note: project ref, keys found (redact in reports as needed), tables/RPCs suspected, auth model.7071---7273## Phase 2 — Client-side surface (anon / user JWT)7475All tests use only **public** credentials (`anon` key + attacker-controlled accounts) unless rules say otherwise.7677### 2A. REST data access7879For each suspected table `T`:8081```http82GET /rest/v1/T?select=*&limit=583apikey: <anon>84Authorization: Bearer <anon_or_user_jwt>85```8687Check:8889| Check | Red flag |90|-------|----------|91| Anon SELECT returns rows | Public data leak / missing RLS |92| Authenticated user reads other users' rows | IDOR / weak USING clause |93| INSERT/UPDATE/DELETE as anon | Anon write |94| INSERT as user A with `user_id` of B | Missing WITH CHECK |95| PATCH role/is_admin/plan columns | Mass assignment / broken column grants |9697Prefer `Prefer: count=exact` and small `limit` — enough for PoC, not dumps.9899### 2B. RPC / functions100101```http102POST /rest/v1/rpc/<fn>103```104105Probe discovered function names. SECURITY DEFINER without auth checks often = privilege escalation.106107### 2C. Auth / JWT108109- Sign up two accounts; compare access to the same resources.110- If policies use roles from JWT: try `updateUser({ data: { role: 'admin' } })` (user_metadata is user-writable).111- Confirm whether admin/premium flags live in `user_metadata` (Critical) vs `app_metadata` / server table.112113### 2D. Storage114115```http116GET /storage/v1/bucket117GET /storage/v1/object/list/<bucket>118```119120- Public buckets with sensitive objects121- Upload to buckets as anon/authenticated when not intended122- Path traversal / predictable object names / IDOR on object paths123124### 2E. Realtime125126- Subscribe to sensitive tables/channels as anon or low-priv user.127- Presence of unrestricted channels for private data.128129### 2F. Edge Functions130131- Call `/functions/v1/<name>` with anon key.132- Missing JWT verification, SSRF, secrets in responses, IDOR in function logic.133134### 2G. Secrets in client135136- `service_role` in JS/mobile → Critical.137- Database password / connection string in client → Critical.138- Long-lived user tokens in logs → High/Medium depending on impact.139140---141142## Phase 3 — Catalog SQL audit (when DB access exists)143144**Read-only.** Do not run DDL/DML "fixes" during bounty unless asked for remediation on owned projects.145146| Access | Script |147|--------|--------|148| Supabase SQL Editor | `references/audit-dashboard.sql` (no `\echo`) |149| `psql` | `references/audit-psql.sql` |150| Deep pass / PT-BR red flags | `references/audit-complete.sql` |151152Run sections in order. Capture scorecard + every CRITICAL/WARN row.153154Interpret with `references/misconfig-catalog.md`.155156---157158## Phase 4 — Abuse validation159160For each candidate finding, prove impact with a minimal PoC:1611621. **Precondition** (keys, account state)1632. **Request(s)** (HTTP or SQL) — redact secrets in final writeup if required1643. **Observed result** (status, sample fields, row counts — not full dumps)1654. **Impact** (confidentiality / integrity / privilege)1665. **False-positive check** (public-by-design marketing data? documented public bucket?)167168Prioritize:1691701. service_role leak, RLS off on tenant tables, USING(true) on private data 1712. user_metadata authorization, anon write, SECURITY DEFINER without checks 1723. Storage IDOR, missing WITH CHECK, horizontal IDOR 1734. Info disclosure (schema, enums, error messages), missing FORCE RLS 174175---176177## Phase 5 — Report178179Write `supabase-security-report-YYYY-MM-DD.md` using `references/report-template.md`.180181Severity guide (adjust to program CVSS):182183| Severity | Examples |184|----------|----------|185| Critical | service_role in client; RLS off + PII/tenant data; USING(true) private tables; user_metadata → admin |186| High | Anon write on sensitive tables; DEFINER RPC priv-esc; Storage full read of private files |187| Medium | IDOR on non-critical objects; missing WITH CHECK; app_metadata misuse if settable |188| Low | Verbose errors; enum/role name leak; RLS perf anti-patterns |189| Info | Hardening notes (FORCE RLS, indexes, Security Advisor) |190191Each finding: title, severity, asset, evidence, PoC, impact, remediation, references (Supabase docs / OWASP).192193---194195## Common vibe-coder failure modes (hunt these first)1961971. **RLS never enabled** on `public` tables exposed via PostgREST 1982. **`CREATE POLICY ... USING (true)`** "so the app works" 1993. **Authorization via `auth.jwt() -> user_metadata` / `raw_user_meta_data`** 2004. **No WITH CHECK** on INSERT/UPDATE (owner spoofing) 2015. **Anon policies for write** left from prototypes 2026. **SECURITY DEFINER** RPCs granted to `anon`/`authenticated` without `auth.uid()` checks or pinned `search_path` 2037. **Storage buckets public** with user uploads 2048. **service_role** in Next.js `NEXT_PUBLIC_*` or mobile apps 2059. **Views** without `security_invoker` bypassing RLS 20610. **Realtime** on tables that REST already protects poorly 207208Full catalog: `references/misconfig-catalog.md`.209210---211212## Remediation patterns (for owned projects or report "fix")213214```sql215-- Enable + force RLS216ALTER TABLE public.t ENABLE ROW LEVEL SECURITY;217ALTER TABLE public.t FORCE ROW LEVEL SECURITY;218219-- Owner-only pattern220CREATE POLICY t_select_own ON public.t221 FOR SELECT TO authenticated222 USING (user_id = (SELECT auth.uid()));223224CREATE POLICY t_insert_own ON public.t225 FOR INSERT TO authenticated226 WITH CHECK (user_id = (SELECT auth.uid()));227```228229- Roles: table `user_roles` with its own RLS, or `app_metadata` set **only** by service_role / Auth Hook — never `user_metadata` for authz.230- DEFINER functions: `SET search_path = public, pg_temp`, explicit authz, minimal GRANT.231- Re-run Supabase Dashboard → Database → **Security Advisor** after fixes.232233---234235## Resource files236237| Path | Use |238|------|-----|239| `references/audit-dashboard.sql` | Catalog audit in SQL Editor |240| `references/audit-psql.sql` | Catalog audit via psql (`\echo` sections) |241| `references/audit-complete.sql` | Extended PT-BR checklist + executive summary query |242| `references/misconfig-catalog.md` | Red flags, severity, PoC hints |243| `references/report-template.md` | Report skeleton |244245## Constraints246247- Read-only preference; no destructive tests without explicit user request.248- Minimize data access; PoC-sized evidence only.249- Do not commit real keys, JWTs, or PII into git.250- Coordinate with `ethical-redteam` / `vuln-discovery` when the engagement is broader than Supabase.