Supabase RLS-Audit
Dieses Skill führt den Datenbank-Teil des vibe-preflight-Scanners aus (scripts/preflight.mjs neben dieser Datei,
Node 20+, keine Abhängigkeiten, kein Netzwerk). Es liest die .sql-Dateien im Projekt (Supabase-Migrationen,
schema.sql), spielt Tabellen, Policies, Views, Funktionen, Grants und Storage-Buckets in der richtigen Reihenfolge ab und meldet
9 Probleme. Es stellt keine Verbindung zu einer Datenbank her.
Ausführung
Vom Projektstammverzeichnis:
node ${CLAUDE_SKILL_DIR}/scripts/preflight.mjs . --only database --format json
Claude Code füllt ${CLAUDE_SKILL_DIR} ein. Andere Agenten: Verwende den Ordner dieses Skills, z.B.
.agents/skills/supabase-rls-audit oder .claude/skills/supabase-rls-audit.
Exit-Code 1 bedeutet mindestens einen Fehler. Füge --fail-on warning hinzu, um auch bei Warnungen fehlzuschlagen, --explain (Textausgabe) für die Begründung und Behebung jedes Problems. JSON: { summary: { error, warning }, findings: [{ rule, severity, message, file, line }] }.
Wenn das Ergebnis 0 SQL-Dateien anzeigt, existieren die Policies wahrscheinlich nur im Dashboard. Bitte den Benutzer,
supabase db pull auszuführen (das schreibt das entfernte Schema in eine Migration) und erneut zu scannen. Melde das Projekt nicht als sauber von einem leeren Scan.
Die Prüfungen
| Regel | Stufe | Bedeutung |
|---|---|---|
rls-disabled | error | Eine public-Tabelle erhält nie enable row level security, daher kann jeder mit dem anon-Key sie über die API lesen und schreiben. |
rls-permissive-policy | error beim Schreiben, warning beim Lesen | using (true) oder with check (true). Bei Insert/Update/Delete kann jeder Benutzer die Zeilen anderer ändern. Bei Select ist jede Zeile öffentlich. |
rls-no-policy | warning | RLS aktiviert, keine Policies: Die API verweigert alles. Gut für reine Server-Tabellen; eine stille leere Liste, wenn der Browser sie liest. |
rls-update-privileged-column | warning | Benutzer können ihre eigene Zeile aktualisieren und sie enthält plan, role, is_admin, credits oder ähnliches. RLS schränkt Zeilen ein, nicht Spalten. |
view-bypasses-rls | error | Ein View ohne security_invoker = true läuft als sein Besitzer und umgeht RLS auf den darunterliegenden Tabellen. |
auth-users-exposed | error | Ein View in public selektiert aus auth.users. |
definer-search-path | warning | Eine security definer-Funktion ohne set search_path. |
definer-function-anon | error / warning | Eine security definer-Funktion in public, die anon unter /rest/v1/rpc/<name> aufrufen kann. Error bei explizitem Grant an anon, Warning bei Supabases Standard-Grant. |
storage-public-bucket | error / warning | Ein öffentlicher Bucket. Error, wenn eine Select-Policy auf storage.objects es auch jedem erlaubt, ihn aufzulisten. |
Behebung
Schreibe immer eine neue Migration. Bearbeite niemals eine, die bereits auf einer Datenbank ausgeführt wurde.
Fehlende oder zu permissive Policies: Frage, welchen Zugriff die App benötigt, und schränke jede Policy auf eine Rolle und den Besitzer ein.
alter table public.notes enable row level security;
create policy "notes: read own" on public.notes
for select to authenticated using ((select auth.uid()) = user_id);
create policy "notes: insert own" on public.notes
for insert to authenticated with check ((select auth.uid()) = user_id);
Privilegierte Spalten: Erlaube Updates nur auf den Spalten, die Benutzer ändern dürfen.
revoke update on public.profiles from authenticated;
grant update (display_name, avatar_url) on public.profiles to authenticated;
Views: alter view public.<name> set (security_invoker = true); (Postgres 15+, alle aktuellen Supabase-Projekte). Ein materialisierter View kann RLS nicht nutzen: Verschiebe ihn aus public oder entziehe das Select-Recht für anon, authenticated. Kopiere bei auth.users die teilbaren Felder mit einem Sign-Up-Trigger in public.profiles.
SECURITY DEFINER-Funktionen: Füge set search_path = '' hinzu und schreibe jeden Namen mit seinem Schema. Schränke dann ein, wer sie aufrufen darf:
revoke execute on function public.admin_set_plan(uuid, text) from public, anon, authenticated;
grant execute on function public.admin_set_plan(uuid, text) to service_role;
Das Entziehen von public allein reicht bei Supabase nicht, da anon einen eigenen Grant auf neue Funktionen hat.
Storage: Mache private Buckets privat, schränke Lesezugriffe auf den Ordner des Besitzers ein
((storage.foldername(name))[1] = (select auth.uid())::text) und serviere signierte URLs. Produktbilder und Logos
können öffentlich bleiben.
Ein Befund, der beabsichtigt ist (ein öffentlicher Katalog, eine reine Server-Tabelle), kann mit
-- vibe-preflight-ignore <rule-id> in derselben Zeile oder der Zeile darüber unterdrückt werden. Schreibe den Grund in denselben Kommentar.
Berichterstattung
- Öffne jede gemeldete Zeile und bestätige sie vor Änderungen am SQL. Der Scanner liest SQL-Text und kann falsch liegen; sage dies, wenn es der Fall ist.
- Gruppiere die Zusammenfassung nach Regel. Zuerst die Fehler, dann die Warnungen, die du mit dem Benutzer bestätigt hast.
- Führe den Scan nach der Behebung erneut aus und berichte das neue Ergebnis.
Grenzen
- Nur das, was in
.sql-Dateien steht. Dashboard-spezifische Änderungen, andere Schemata und was eine Funktion innerhalb ihres Rumpfes tut, werden nicht geprüft. - Ein sauberes Ergebnis bedeutet, dass diese 9 Muster nicht gefunden wurden. Es beweist nicht, dass die Policies den Regeln der App entsprechen. Tests als echter anon- und eingeloggter Benutzer (der Supabase-Client mit dem anon-Key) sind weiterhin empfehlenswert.
- Für die Browser- und Umgebungsseite (service_role-Key im Client-Code, Geheimnisse in
NEXT_PUBLIC_-Variablen, eingecheckte.env) führe den vollständigen Scan ohne--onlyaus oder verwende das Skillvibe-preflight.