// TECHNOLOGIES / SUPABASE

Supabase

Supabase, proven from the outside.

Supabase ships a genuinely good in-dashboard Security Advisor — but it reads the database schema, so it never sees your application code, your client bundle, or the deployed runtime. NewScan scans from the attacker's seat: with only your public URL and anon key it proves the exploitable misconfigurations the Advisor structurally can't — a leaked service_role key, tables readable through the Data API, INSERT policies missing WITH CHECK, anon-callable RPCs, and public storage. Every signal is reproduced before it is recorded.

The Advisor blind-spot

// why an external scan matters

The Advisor analyses the schema from inside the dashboard, and only warns — in a panel most vibe-coders never open — about schema shape, not live exploitability. An entire class of critical, real-world-exploited issues lives in the shipped client code, the bundle, or the app's design, outside its detection scope by design. NewScan proves those from the public URL + anon key: the attacker's exact position, and exactly the evidence an auditor wants.

// THE ADVISOR SEES (SCHEMA)

RLS disabled on a table, a permissive USING (true) policy, a SECURITY DEFINER view, GraphQL exposure — all warned in-dashboard as schema shape.

// THE ADVISOR CAN'T SEE (NEWSCAN DOES)

A service_role key shipped in the browser bundle, a missing WITH CHECK clause, whether a table actually returns data to anon, or an RPC that runs without an auth guard.

service_role key in the client bundle

// tool: scan_supabase  critical

The single most catastrophic Supabase mistake. The service_role key carries Postgres BYPASSRLS — any holder reads, writes, and deletes every row in every table and reaches the auth schema (hashed passwords, recovery tokens → account takeover), regardless of RLS. It leaks silently: build, deploy, and app all succeed. NewScan fetches the deployed bundles, finds every JWT, and decodes the role claim.

// WHY THIS IS FALSE-POSITIVE-FREE

A finding is recorded only when a bundle JWT base64-decodes to role === "service_role" (or a new-format sb_secret_ key). The anon key is meant to be public and is never flagged — the role claim is unambiguous, so recording on decode alone can't false-positive.

GET /assets/app.[hash].js regex eyJ… JWT → base64url-decode the payload → {"role":"service_role", …} # critical: rotate + move server-side

Live data exposure via the Data API

// tool: enumerate_supabase

A table with RLS disabled, or a USING (true) / any-authenticated policy, is fully readable through auto-generated PostgREST at /rest/v1/<table> using only the anon key. NewScan confirms exposure by row count, never by exfiltration.

// COUNT-SAFE EXPOSURE PROBE

GET /rest/v1/<table>?select=*&limit=1 Prefer: count=exact Range: 0-0 -> Content-Range: 0-0/1240 # reach proven, no rows pulled

// THE LOAD-BEARING FP GUARD

Data or a non-zero count = exposed. An empty array is not a finding — RLS-enabled-with-no-policy legitimately returns [] and is the secure state. NewScan never flags an empty result, the biggest false-positive trap in naive Supabase scanners.

Missing WITH CHECK & anon RPCs

// non-destructive · no Advisor lint

A policy with a USING clause but no WITH CHECK lets unauthorised rows be written — the Advisor has no lint for it, and developers routinely test only SELECT. And a database function at /rest/v1/rpc/<fn> is anon-callable unless it guards on auth.uid(). Both are proven without ever creating a row or running a destructive function.

// WRITE AUTHORISATION DIFFERENTIAL  high

POST /rest/v1/payments # anon, deliberately invalid body -> 400 null value … violates not-null # write AUTHORISED -> 401 / 403 # RLS blocked (secure)

Only the 400 DB-constraint vs 401/403 differential records — a bare 400 does not. The write fails on validation, so no row is ever created.

// ANON-CALLABLE RPC  high

POST /rest/v1/rpc/is_admin # anon key, empty args -> 404 # no such function (secure/absent) -> 200 / 400 # exists & anon-invocable → finding

A SECURITY DEFINER function callable by anon runs as the owner and bypasses RLS — unauthenticated privilege escalation.

What NewScan records

// class · method · severity
Finding How NewScan finds it Severity
service_role key leaked A client-bundle JWT decodes to role=service_role (or an sb_secret_ key). critical
Table live-exploitable /rest/v1/<table> returns data or a non-zero count to the anon key. high
Missing WITH CHECK An anon INSERT is authorised (400 DB-constraint, not 401/403) — no row created. high
Anon-callable RPC A sensitive /rest/v1/rpc/<fn> exists (200/400, not 404) with the anon key. high
Public storage bucket An anonymous /storage/v1/object/list/<bucket> returns a non-empty listing. medium

Recent Supabase security issues

// why these checks matter

The vulnerability classes above, seen at scale in the wild — AI-generated apps shipping tables with no RLS, and real projects anonymously leaking user data.

External links; referenced for context. CVE severities and details are the publishers'.

How it's recorded

// audit-ready evidence

CATEGORY

The service_role leak maps to OWASP API2; table/bucket exposure to API1; missing WITH CHECK to API3; anon RPC to API5.

SEVERITY

One calibrated severity per finding that translates into each framework's risk rating and remediation SLA — no inflated numbers.

EVIDENCE

Every finding stores the exact request and the decoded role, the count, or the DB-constraint differential that proved it — never a row of your users' data.

Point it at your Supabase app.

NewScan is free and self-hosted — bring your own key. Scanning your own Supabase project is local to you, so it never depends on any hosted resource. The service_role, RLS-exposure, write, and RPC checks run in the deterministic floor with no model or provider key at all.