supabase-security
Skill0 starsChanged 42 days agoApache-2.0View on GitHub
Security review and secure-coding guidance for Supabase apps (Postgres with RLS, Auth, Storage, Edge Functions, Realtime), with web, mobile and SSR clients. Use when auditing or hardening a Supabase project, or when writing migrations, RLS policies, grants, database functions (RPC), views, storage buckets, Edge Functions, Realtime channels, auth settings, or client code using supabase-js or @supabase/ssr. Triggers on a supabase/ directory, supabase/migrations/*.sql, supabase/config.toml, supabase/functions/, @supabase/supabase-js or @supabase/ssr in package.json, and SUPABASE_URL or Supabase key env vars. Covers publishable, secret, anon and service_role keys, Data API exposure and grants, RLS policy bugs, multi-tenant isolation, SECURITY DEFINER functions and views, Storage policies, auth configuration (redirect URLs, email confirmation, MFA, anonymous users), getSession vs getClaims, Edge Function auth and SSRF, Realtime authorization, pg_net, cron and Vault, and advisor and pgTAP verification.
- Reads credentials
1 other project uses a version of this.
Adopted by
Most recently taken up by
- narlyseorg/superhackerstheir own version, first seen Nov 8, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- narlyseorg/superhackers★ 8same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository11 other files it gives its agents.
- appsec-reviewSkill
- aspnet-core-securitySkill
- django-securitySkill
- flutter-securitySkill
- laravel-securitySkill
- nestjs-securitySkill
- nextjs-securitySkill
- nodejs-securitySkill
- react-securitySkill
- spring-boot-securitySkill
- wordpress-securitySkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
14 rules. About 0 are common, 4 are distinctive, and 10 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
10
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 18
All paths in this skill (references/...) are relative to the directory containing this SKILL.md.
- Very specific: line 25
If the appsec-review skill is installed, it owns the overall methodology and report format.
- Very specific: line 36
Read installed versions from the lock file: @supabase/supabase-js, @supabase/ssr (or deprecated @supabase/auth-helpers-*), …
- Very specific: line 42
Data API: every table, view, materialized view and function in exposed schemas ([api] schemas, default public, graphql_public), with grants …
- Very specific: line 70
Every finding needs evidence: the migration file and line (or code file:line), the exact SQL/code, which role can reach it (anon, any …
- Very specific: line 74
Write the fix as a new migration (never edit applied migrations), with grants, RLS and policies together.
- Distinctive: line 109
Do not report these without further evidence:
- Distinctive: line 142
When writing Supabase code, default to:
- Very specific: line 148
Functions: prefer security invoker. For security definer, pin search_path = '', check the caller inside, revoke execute ...
- Very specific: line 153
Edge Functions: keep verify_jwt on for user-called functions and still verify the user in code (@supabase/server auth: 'user').
- Very specific: line 156
Add a pgTAP test per table and function you secure (anon, owner, other user, other tenant), and run supabase db advisors --local --type …
- Very specific: line 160
Use these when appsec-review is not installed.
- Distinctive: line 166
Hardening: not demonstrably exploitable, but a stronger control is recommended.
- Distinctive: line 192
Default grants change (2026): https://github.com/orgs/supabase/discussions/45329
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Supabase Security
Find, explain, fix and verify security issues in Supabase-backed applications, and write Supabase schema, policies, functions and client code that do not introduce them.
All paths in this skill (references/...) are relative to the directory containing this SKILL.md.
Modes
- Review mode: audit, security review, "is my RLS right?", pre-launch check. Follow the workflow below and produce findings.
- Build mode: writing migrations, policies, functions, buckets, Edge Functions or client code. Apply the build-mode guardrails to every change and load only the reference for the area you touch.
If the appsec-review skill is installed, it owns the overall methodology and report format; this skill supplies the Supabase knowledge. For framework-general issues in the app around Supabase, use nextjs-security or nodejs-security if installed.
The Supabase security model in one paragraph
Clients talk to Postgres directly through the Data API with a public key, so grants and RLS policies are the authorization layer. The publishable/anon key identifies the app, not the user, and is meant to be public. Each request runs as anon, authenticated (any signed-in user, including anonymous users) or service_role (secret key, bypasses RLS). A table in an exposed schema that a client role can reach without correct RLS is an open database. Everything else (Storage, Realtime, RPC functions, views) inherits from, or bypasses, that same model.
Review workflow
1. Confirm the stack and versions
- Confirm Supabase:
supabase/withconfig.tomlandmigrations/,supabase/functions/,@supabase/*packages,SUPABASE_URL/*_SUPABASE_*env vars, or a self-hosteddocker-composewithsupabase/*images. - Read installed versions from the lock file:
@supabase/supabase-js,@supabase/ssr(or deprecated@supabase/auth-helpers-*),@supabase/server,@supabase/auth-js. For self-hosted, read image tags forsupabase/gotrue,supabase/realtime,supabase/storage-api,supabase/postgres. - Note which key generation is in use (
sb_publishable_/sb_secret_vs legacyanon/service_roleJWTs; legacy keys are being deprecated, with removal planned for late 2026 (date to be confirmed); projects created since November 2025 may not have them) and the project age: new tables are no longer auto-granted to client roles on new projects (from 2026-05-30) and on all projects from 2026-10-30. Seereferences/data-api-exposure.md. - Note what is not in the repo: hosted Dashboard settings (auth, Realtime public access, exposed schemas) and changes made outside migrations. Findings that depend on them are Likely until confirmed.
2. Map the attack surface
- Data API: every table, view, materialized view and function in exposed schemas (
[api] schemas, defaultpublic,graphql_public), with grants and RLS. Also GraphQL ifpg_graphqlis enabled. - Storage: buckets (
storage.bucketsinserts,[storage.buckets.*],createBucket) andstorage.objectspolicies. - Realtime:
realtime.messagespolicies,.channel(usage,supabase_realtimepublication. - Edge Functions:
supabase/functions/*/index.ts,[functions.*] verify_jwt, deploy flags. - Server code with the secret key: API routes, Server Actions, workers, cron, webhook handlers.
- Auth configuration:
config.toml [auth*], redirect URLs, hooks, providers; database jobs (cron.schedule,net.http_*, webhooks).
3. Review each area
Load the reference for each area as you reach it.
| Area | Reference | Start by looking for |
| Keys and secrets | references/api-keys-secrets.md | SERVICE_ROLE/sb_secret_/SECRET_KEY under NEXT_PUBLIC_/VITE_/EXPO_PUBLIC_, admin clients imported by client code, eyJ values, legacy JWT secret |
| Data API exposure | references/data-api-exposure.md | create table without enable row level security, grant ... to anon, on all tables in schema, materialized views, extra exposed schemas |
| RLS policies | references/rls-policies.md | using (true), missing to, user_metadata, update policies without column limits, share_token is not null, permissive OR |
| Multi-tenancy | references/multi-tenancy.md | Membership table insert/update policies, org_id on writes, membership helpers, JWT tenant claims |
| Functions, views, triggers | references/functions-views-triggers.md | security definer in public, missing set search_path, execute grants, views without security_invoker, auth.users in views, dynamic SQL |
| Storage | references/storage.md | public = true buckets with private files, bucket-only policies, storage.foldername, signed URL expiry, MIME/size limits |
| Auth configuration | references/auth-config.md | enable_confirmations, redirect wildcards, site_url, anonymous sign-ins, MFA enforcement, hooks |
| Server and SSR clients | references/server-ssr-clients.md | getSession() on the server, admin client with request filters, .or( with input, shared clients, cached Set-Cookie |
| Edge Functions | references/edge-functions.md | verify_jwt = false, service role + body IDs, missing caller check, fetch(body.url), webhook signatures |
| Realtime | references/realtime.md | Channels without private: true, realtime.messages policies, replica identity full, broadcasts from triggers |
| Webhooks, cron, Vault | references/webhooks-cron-vault.md | Literal keys in cron.schedule/net.http_post, URLs from input, vault.decrypted_secrets exposure |
| Verification | references/verification.md | Advisors, catalog queries, pgTAP tests, safe curl checks |
4. Classify and report
Every finding needs evidence: the migration file and line (or code file:line), the exact SQL/code, which role can reach it (anon, any authenticated, members, admins), and what the attacker gets. Read migrations in order; later ones can undo earlier fixes.
5. Remediate and verify (when asked to fix)
Write the fix as a new migration (never edit applied migrations), with grants, RLS and policies together. Prove it with a pgTAP test that fails before and passes after, rerun the security advisors, and fix siblings. See references/verification.md.
High-signal patterns
These are investigation signals, not findings. Trace each one.
# Exposure and grants (SQL)
create table (no matching "enable row level security") disable row level security grant all grant .* to anon
on all tables in schema public to alter default privileges .* grant create materialized view schemas = [
# Policies (SQL)
using (true) with check (true) create policy ... (no "to") for all user_metadata raw_user_meta_data
auth.role() auth.email() is not null) ->> 'email') like for update ... using (...) (no with check, privileged columns)
on org_members for insert on profiles for update as permissive (multiple permissive policies per command)
# Functions, views, triggers (SQL)
security definer (no "set search_path") language plpgsql ... execute format('%s' || p_ create view (no security_invoker)
join auth.users from auth.users raw_user_meta_data ->> 'role' vault.decrypted_secrets net.http_post( cron.schedule(
# Storage (SQL / client)
insert into storage.buckets public = true 'public', true on storage.objects bucket_id = ' createSignedUrl( expiresIn
# Client and server code
SERVICE_ROLE sb_secret_ SUPABASE_SECRET_KEY NEXT_PUBLIC_.*(SERVICE|SECRET) createClient(.*SERVICE .auth.admin.
auth.getSession() (server-side) user_metadata .or(` .filter(req .eq('org_id', req createServerClient(.*SECRET
.channel( (no private: true) track(
# Edge Functions / config.toml
verify_jwt = false --no-verify-jwt SUPABASE_SERVICE_ROLE_KEY await req.json() fetch(body enable_confirmations = false
additional_redirect_urls *.vercel.app ** enable_anonymous_sign_ins = trueCommon false positives
Do not report these without further evidence:
- Publishable or
anonkey in client code, mobile apps orNEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY/NEXT_PUBLIC_SUPABASE_ANON_KEY. Public by design. Review RLS instead. - RLS enabled with no policies. Deny-all for client roles; correct for server-only tables (advisor
0008is INFO). to anon, authenticated using (true)on intentionally public, read-only data (announcements, published content, catalogs) with no write policies.security definerfunctions withset search_path = ''in a non-exposed schema (private) withexecuterevoked frompublic/anon, used as policy helpers. Also definer functions that checkauth.uid()/membership first.- Trigger functions (
returns trigger) such ashandle_new_user: not callable over RPC. A finding only if they copy user-controlled metadata into privileged columns. - Views with
security_invoker = true(Postgres 15+), or views not granted to client roles. verify_jwt = falseon webhook receivers that verify a provider signature, or on deliberately public endpoints.Access-Control-Allow-Origin: *on Edge Functions that authenticate with bearer headers rather than cookies.- Public buckets for public assets (avatars, product images) when uploads are restricted to the owner's folder.
auth.uid()not wrapped in(select ...): a performance issue, not a vulnerability.- Secret/
service_rolekey read from server-only env (Deno.env.get, non-prefixedprocess.envin server modules). config.tomllocal defaults (enable_confirmations = false, localhost redirect URLs) when nothing indicates the hosted project uses them and nothing trusts the email claim.
Severity calibration
| Who can exploit it | Typical severity for data access or privilege bugs |
Anyone with the public key (anon), including unauthenticated visitors | Critical/High |
Any signed-up user (authenticated) while sign-up is open (the default), including anonymous users if enabled | High (Critical for cross-tenant data or privilege escalation) |
| Members of the same tenant | Medium/High (by data and action) |
| Tenant or platform admins | Usually Low, unless it crosses tenants |
Common under-ratings to avoid:
- "Requires authentication" is not a mitigation when anyone can sign up. Check
enable_signupand email confirmation before lowering severity. - Self-join of tenants, self-promotion via a role column, and
user_metadatatrust are privilege escalations: rate the resulting access (usually Critical), not the "policy typo". - A
SECURITY DEFINERfunction or definer view over user data exposed to client roles is an RLS bypass for every row it can return. - Secret key exposure is Critical regardless of RLS quality.
Build-mode guardrails
When writing Supabase code, default to:
- Every exposed table in one migration:
create table→alter table ... enable row level security→ explicit minimalgrants per role → one policy per command withto authenticated(orto anononly for public data). No blanketgrant ... on all tables. - Policies:
usingfor reads/targets,with checkfor new values;(select auth.uid()); nouser_metadata, noauth.role(); restrictive policies for MFA (aal2) and anonymous-user limits. - Privileged columns (
role,org_id,user_id,plan,credits) are not client-updatable: column grants, a separate admin-managed table, or a guard trigger. - Tenancy: membership rows are written only by admins or a validated invite function. Helpers are
security definer,set search_path = '',stable, inprivate, executable byauthenticatedonly. - Functions: prefer
security invoker. Forsecurity definer, pinsearch_path = '', check the caller inside,revoke execute ... from public, anon, and grant to the narrowest role. No string-built dynamic SQL. - Views:
with (security_invoker = true); never exposeauth.users; no materialized views to client roles. - Storage: private buckets by default, per-user or per-tenant path policies on every command,
file_size_limitand exactallowed_mime_types, short-lived signed URLs issued after an authorization check. - Keys: publishable key in clients; secret keys only in server-only modules and Edge Function env, never under a client-exposed prefix, never in migrations (use Vault). Separate named secret keys per component.
- Server code: user-scoped
createServerClientper request with the publishable key; authorize withgetClaims()/getUser(), nevergetSession(); admin client only after an explicit authorization check, with filters derived from verified identity; no user input in.or()/.filter()strings. - Edge Functions: keep
verify_jwton for user-called functions and still verify the user in code (@supabase/serverauth: 'user'); webhooks verify signatures on the raw body; validate input; block private-network URLs on outbound fetches. - Realtime:
private: truechannels,realtime.messagespolicies that check membership for the topic, public access disabled. - Auth: email confirmation on, exact production redirect URLs, Site URL set, CAPTCHA on public sign-up, leaked-password protection where available, MFA enforced in RLS where required.
- Add a pgTAP test per table and function you secure (anon, owner, other user, other tenant), and run
supabase db advisors --local --type securitybefore merging.
Evidence and reporting rules
Use these when appsec-review is not installed.
Classification
- Confirmed: the path from the attacker's role to the data or action is fully traced in migrations/code (grants, RLS state, policies, function body), or safely demonstrated on a local stack.
- Likely: one condition could not be verified (hosted settings, whether a migration was applied, Dashboard-made changes). Name it.
- Hardening: not demonstrably exploitable, but a stronger control is recommended.
- Informational: context with no direct security impact.
Finding format
### [SEVERITY] Title — Confirmed|Likely|Hardening
- **Location:** `supabase/migrations/20260801_init.sql:42` (policy "..." on public.documents)
- **Evidence:** the SQL/code, and which role reaches it (anon / any authenticated / member)
- **Impact:** what the attacker reads, changes or becomes
- **Preconditions:** sign-up open? hosted setting? key in use?
- **Fix:** new migration or code change (snippet)
- **Verify:** pgTAP test, advisor result, or curl with the publishable key
- **Refs:** CWE / OWASP / Supabase docs linkRules: never invent tables, policies, settings or versions. Redact keys (sb_secret_****; for JWTs show only the role claim). Say explicitly when runtime verification was not performed. Only test projects the user is authorized to assess, prefer the local stack, and never use production data or destructive requests to prove a finding.
References
- Supabase security docs: https://supabase.com/docs/guides/security/product-security
- Row Level Security: https://supabase.com/docs/guides/database/postgres/row-level-security
- Securing the Data API: https://supabase.com/docs/guides/api/securing-your-api
- API keys: https://supabase.com/docs/guides/getting-started/api-keys
- Advisors (database linter): https://supabase.com/docs/guides/database/database-advisors
- Production checklist: https://supabase.com/docs/guides/deployment/going-into-prod
- Default grants change (2026): https://github.com/orgs/supabase/discussions/45329
- OWASP API Security Top 10 2023: https://owasp.org/API-Security/editions/2023/en/0x11-t10/
- OWASP ASVS 5.0: https://owasp.org/www-project-application-security-verification-standard/
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

