跳到正文
DEV Community· PatchAndShip·· 2 小時前AI 評分72

Supabase 上 AI 輔助編碼應用的 12 項安全檢查清單(2026)

A 12-step security checklist for vibe-coded apps on Supabase (2026)

AI 導讀

PatchAndShip 提出一份適用於以 AI 工具構建、採用 Supabase 的應用的 12 項安全檢查清單,協助在迎接真實用戶前核查資料保護設定。

正文

You built an app with Lovable, Bolt, Replit, v0 or Cursor on top of Supabase, and it works in preview. Before real users show up, here's how to check whether their data is actually protected. For each step you get what to check, how to check it yourself, and what the fix looks like.

One rule before we start: run every check against an app you own. Don't test someone else's app without permission, not even with a single curl.

Short version: Row Level Security (RLS) on for every table in public, every policy limited to the row's owner, no secret key in your frontend or repo, private files in private buckets, and Site URL pointing at production. Then test it all while logged out.

What the scans found

Two security vendors published scans this year. Both sell scanning tools, and they measured different things, so don't compare their numbers against each other.

Sentris (public code repositories, published Aug 5, 2026; vendor research): Sentris scanned 2,144 public repositories that use Supabase, not live apps. In Sentris's scan of public GitHub repositories, 57% of Lovable repos had a table in the public schema with Row Level Security switched off, compared with 21.7% of Supabase repos with no AI-tool marker. Sentris says this one check accounts for almost all of the gap. Leaked credentials showed up in 11.3% of Lovable repos and 11.2% of repos with no AI-tool marker, "the same rate, within noise". A public storage bucket turned up in 38.7% of Lovable repos, compared with 10.5% of repos with no AI-tool marker. (Sentris)

Symbiotic Security (live sites, published Jun 2, 2026; vendor research): In Symbiotic Security's scan of 1,072 vibe-coded sites that use Supabase, 172 let anyone delete database records without authentication, 172 allowed unauthenticated PATCH (modification), 39 had tables fully readable with the public Supabase key, and 14 allowed unauthenticated inserts. (Symbiotic Security)

Per Sentris, the gap between Lovable repos and repos with no AI-tool marker comes almost entirely from tables with RLS switched off, not from leaked credentials. So that's where we start.

How Supabase security works, in one paragraph

Your frontend talks to Supabase directly, using a key that ships in your JavaScript. Publishable (anon) keys are meant to be public. What actually protects your data is RLS together with grants: grants control whether a role can reach a table, RLS controls which rows it can access. The other key is a different story. Supabase: "A secret key bypasses every Row Level Security policy you have. Never put one in a browser, a shipped application, or source control." (API keys, Securing your API)

So "is my key exposed?" is usually the wrong question. The better questions are "which key?" and "what do my policies allow?"

The 12 checks

1. Every table in public has RLS enabled

select c.relname as table_name
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
  and c.relkind = 'r'
  and not c.relrowsecurity;

Any table this returns has RLS off. The Security Advisor flags this as an error (lint 0013): "Anyone with your project URL can read, edit, and delete all data in this table because Row-Level Security is not enabled." (Database Advisors)

Fix: alter table public.your_table enable row level security; and add the policies from step 2 in the same migration. Supabase: "Once RLS is enabled, no data is accessible through the API when using a publishable key, until you create policies." (Row Level Security)

2. Policies are per operation and scoped to the owner

The Advisor flags always-true conditions such as using (true) as a permissive policy (lint 0024, a warning). And when a request has no signed-in user, auth.uid() returns null (Row Level Security).

Our guidance: using (true) lets everyone in, and auth.uid() is not null on its own lets any signed-in user reach every other user's rows. Compare auth.uid() to the owner column:

create policy "orders_select_own" on public.orders
  for select to authenticated
  using ((select auth.uid()) = user_id);

create policy "orders_insert_own" on public.orders
  for insert to authenticated
  with check ((select auth.uid()) = user_id);

create policy "orders_update_own" on public.orders
  for update to authenticated
  using ((select auth.uid()) = user_id)
  with check ((select auth.uid()) = user_id);

create policy "orders_delete_own" on public.orders
  for delete to authenticated
  using ((select auth.uid()) = user_id);

For updates, using decides which existing rows can be updated and with check decides what the resulting row may look like, which stops a user from reassigning user_id. If with check is omitted, using applies to both (PostgreSQL: CREATE POLICY). We write both so the rule is explicit. Wrapping it as (select auth.uid()) lets Postgres cache the result per statement via an initPlan, which improves RLS performance (RLS performance and best practices).

Check: sign in as user A, grab the ID of a record owned by user B, and try to load, edit and delete it. Every attempt should fail.

3. Views don't quietly bypass RLS

The Advisor flags a security definer view as an error (lint 0010). Views bypass RLS by default because they run with the owner's permissions, usually postgres in Supabase. On Postgres 15+:

create view public.public_profiles
with (security_invoker = true) as
select id, display_name, avatar_url from public.profiles;

On older versions, revoke access from anon and authenticated, or move the view to an unexposed schema (Row Level Security, PostgreSQL: CREATE VIEW).

4. The logged-out test returns only public data

In your live app, open DevTools, then Network, find a request to https://<project>.supabase.co/rest/v1/... and copy the apikey header. Then:

curl "https://<project>.supabase.co/rest/v1/<table>?select=*&limit=5" \
  -H "apikey: <public-key>"

Private tables should return [] or a permission error. If one returns rows, anyone can do what you just did. Own apps only.

5. No secret key in the frontend or the repo history

  • Search the built bundle: npm run build, then grep -rE "sk_live_|sb_secret_|service_role|c2VydmljZV9yb2xl|NlcnZpY2Vfcm9s|zZXJ2aWNlX3Jv" dist/. The last three patterns catch a legacy service_role key: it's a JWT (starts with eyJ), so the word "service_role" only appears base64-encoded inside it.
  • Vite: "Variables prefixed with VITE_ will be exposed in client-side source code after Vite bundling." (Vite)
  • History: git log -p --all | grep -E "sk_live_|sb_secret_|service_role|c2VydmljZV9yb2xl|NlcnZpY2Vfcm9s|zZXJ2aWNlX3Jv".

If a secret was committed, GitHub says to revoke or rotate it first. Removing it means rewriting history, and old commits may still be reachable in clones, forks, cached views and pull requests (GitHub Docs).

Fix order (our guidance): move the code to a server (Edge Function or API route), create a new key, deploy it everywhere, then revoke the old one. Revoke last, or you'll break production mid-deploy.

6. Storage buckets match what's in them

Buckets "are private by default". For a public bucket, "anyone who possesses the asset URL can readily access the file" (Storage Buckets). Check which buckets are Public, and whether any of them hold per-user files like IDs, invoices or exports. Make those private and add policies on storage.objects.

7. New public tables need explicit grants. Never GRANT ALL to anon

From Oct 30, 2026, once the change reaches your project, new tables in public need an explicit grant before the Data API can reach them; existing tables keep their current grants. New projects have had this by default since the May 30, 2026 rollout (Supabase changelog). The risky quick fix is grant all ... to anon: once a grant exists, RLS is the only guard.

More on this: GRANT ALL to anon is the wrong fix for Supabase 42501 after Oct 30

Our rule: grant only what each role needs, keep RLS on with policies, and never GRANT ALL to anon. Put the grant, RLS and policies in one migration:

begin;
alter table public.orders enable row level security;
revoke all on table public.orders from anon, authenticated;
grant select, insert, update, delete on table public.orders to authenticated;
-- plus the four policies from step 2
commit;

8. Auth URLs point at production

Supabase: change the Site URL "from http://localhost:3000 to your production URL"; it's "critical for email confirmations and password resets". Use exact redirect URLs in production, not wildcards (Redirect URLs).

9. Payment state comes from verified webhooks

If the browser can mark a user "paid", so can anyone else. Set the flag only in a server-side handler for a verified Stripe webhook, and verify the Stripe-Signature header first. Stripe says endpoints "might occasionally receive the same event more than once", so log processed event IDs and skip repeats (Stripe: Webhooks).

10. Edge Functions check who's calling

OWASP Top 10:2025 keeps Broken Access Control at #1 (A01), including "an accessible API with missing access controls for POST, PUT, and DELETE" (OWASP). For each function, ask what stops a stranger from calling it. The answer should be a verified user JWT (plus a per-record check), a verified webhook signature, or a shared secret for cron. "Nobody knows the URL" doesn't count. Supabase's default is verify_jwt = true; leave it on for user-facing functions. For Stripe-style webhooks, keep verify_jwt = false and verify the signature in the handler (Functions auth).

11. Rate limits on anything that costs money or sends mail

Our guidance: endpoints that call a paid AI API, send email or SMS, or create accounts can be run up by a script. Add CAPTCHA or bot protection to sign-up if you see abuse, cap per-user usage on the server, and set spending limits with your providers.

12. You know what's deployed

Preview, repo and production drift apart over time. After any fix, re-run the logged-out test (step 4) and the two-account test (step 2) against production.

Got 15 minutes?

  • Run the RLS query in step 1.
  • Run the logged-out curl against your three most sensitive tables.
  • Run the two greps in step 5 (they cover sb_secret_, sk_live_ and legacy service_role JWTs).
  • Check which buckets are public.
  • Add one line to your AI tool's project instructions: Every new table must enable RLS, grant only needed operations, and have one owner-scoped policy per operation, in the same migration.

This isn't a penetration test, a compliance audit or a guarantee. Apps that handle health, payment-card or children's data need more than this.

Sources

Checked Oct 27, 2026. Lines marked "our guidance" are ours, not the sources'.


The full checklist, with the CVE background and more detail on each step, is on our site. If you'd like a second look, there's a free check.

Not affiliated with Supabase.

PatchAndShip Team

來源:DEV Community · dev.to