FixVibe

// docs / baas security / supabase storage

Supabase-Storage-Bucket-Sicherheits-Checkliste: 22 Punkte

Supabase Storage ist eine dünne Schicht über einem S3-kompatiblen Bucket plus dasselbe Row-Level-Security-Modell wie die Datenbank. Das heißt, die gleichen RLS-Fallstricke, die Tabellen betreffen, betreffen den Dateizugriff — plus ein paar Storage-spezifische, die auftauchen, wenn KI-Coding-Tools Uploads verdrahten. Diese Checkliste umfasst 22 Punkte in fünf Sektionen: Bucket-Konfiguration, RLS-Policies, Upload-Validierung, Signed URLs und operative Hygiene. Jeder ist in unter 15 Minuten verifizierbar.

Jeder Punkt unten ist essenziell. Für die zugrunde liegenden RLS-Mechaniken siehe Supabase-RLS-Scanner. Für die Key-Offenlegungsklasse angrenzend an Storage siehe Supabase-Service-Role-Key in JavaScript offengelegt.

Bucket-Konfiguration

Beginne mit den richtigen Defaults. Ein falsch konfigurierter Bucket leakt Dateien, egal ob dein RLS korrekt ist oder nicht.

  1. Jeder Bucket standardmäßig privat. Im Supabase-Dashboard → Storage → Buckets, schalte den Public bucket-Toggle aus, sofern du keinen expliziten Grund hast (Marketing-Assets, öffentliche Avatare ohne PII). Public Buckets umgehen RLS für Lesezugriffe — jeder mit dem Bucket-Namen kann auflisten und herunterladen.
  2. Setze ein hartes Dateigrößen-Limit auf jeden Bucket. Dashboard → Bucket-Einstellungen → Dateigröße-Limit. 50 MB ist ein vernünftiger Default für Nutzer-Uploads; erhöhe es bewusst für Video- / Großdatei-Anwendungsfälle. Ohne Limit kann ein einziger böswilliger Upload deine Storage-Quote oder deine monatliche Bandbreite aufbrauchen.
  3. Beschränke erlaubte MIME-Types pro Bucket. Erlaubte-MIME-Types-Liste — explizite Allowlist, keine Blocklist. image/jpeg, image/png, image/webp für Bild-only-Buckets. Erlaube nie text/html, application/javascript oder image/svg+xml in einem Nutzerinhalt-Bucket — sie führen im Browser aus, wenn sie via Signed URL ausgeliefert werden.
  4. Verwende einen Bucket pro Inhaltstyp, nicht einen geteilten Bucket. Per-Bucket-Einstellungen (Größe, MIME-Types, RLS-Policies) sind die Granularität, die du hast. Ein user-avatars-Bucket, ein document-uploads-Bucket und ein public-assets-Bucket sind einfacher zu sperren als ein gemischter Bucket.
  5. Verifiziere die CORS-Konfiguration bei Frontend-Uploads. Wenn Nutzer direkt aus dem Browser an eine Signed URL hochladen, muss die CORS-Konfiguration des Buckets deinen Produktions-Origin listen. * ist nur für öffentliche Buckets akzeptabel — niemals für Buckets mit Nutzer-PII.

RLS-Policies auf storage.objects

Supabase Storage speichert Datei-Metadaten in der Tabelle storage.objects. RLS auf dieser Tabelle steuert, wer Dateien lesen, hochladen, aktualisieren oder löschen kann. Ohne RLS ist die Public/Private-Flag des Buckets dein einziger Schutz.

  1. Bestätige, dass RLS auf storage.objects aktiviert ist. SELECT rowsecurity FROM pg_tables WHERE schemaname = 'storage' AND tablename = 'objects'; muss true liefern. Supabase aktiviert es standardmäßig bei neuen Projekten; verifiziere, dass es nicht deaktiviert wurde.
  2. Schreibe eine SELECT-Policy auf auth.uid() für private Buckets eingegrenzt. CREATE POLICY "users_read_own_files" ON storage.objects FOR SELECT USING (auth.uid()::text = (storage.foldername(name))[1]);. Konvention: speichere Dateien unter [user-id]/[filename] und nutze storage.foldername(), um den Eigentümer aus dem Pfad zu extrahieren.
  3. Schreibe eine INSERT-Policy, die dieselbe Pfadkonvention erzwingt. CREATE POLICY "users_upload_own" ON storage.objects FOR INSERT WITH CHECK (auth.uid()::text = (storage.foldername(name))[1]);. Ohne WITH CHECK kann ein authentifizierter Nutzer in den Ordner eines anderen Nutzers hochladen.
  4. Füge UPDATE- und DELETE-Policies hinzu, wenn deine App Dateiänderungen oder -löschungen unterstützt. Jeder Befehl braucht seine eigene Policy. DELETE auszulassen bedeutet, dass authentifizierte Nutzer ihre eigenen Dateien nicht entfernen können; UPDATE auszulassen bedeutet, dass Datei-Überschreibungen lautlos fehlschlagen.
  5. Teste den nutzerübergreifenden Zugriff in zwei Browser-Sessions. Melde dich als Nutzer A an, lade eine Datei hoch, kopiere den Pfad. Melde dich als Nutzer B in einem anderen Browser an, versuche, die Datei über die REST-API zu holen. Die Antwort muss 403 oder 404 sein, niemals 200.
sql
-- Confirm RLS on storage.objects
SELECT rowsecurity
FROM   pg_tables
WHERE  schemaname = 'storage' AND tablename = 'objects';

-- SELECT policy: scope reads to the owning user's folder.
CREATE POLICY "users_read_own_files"
  ON storage.objects
  FOR SELECT
  USING (auth.uid()::text = (storage.foldername(name))[1]);

-- INSERT policy: enforce the [user-id]/[filename] path convention.
CREATE POLICY "users_upload_own"
  ON storage.objects
  FOR INSERT
  WITH CHECK (auth.uid()::text = (storage.foldername(name))[1]);

Upload-Validierung

Validiere jeden Upload serverseitig, selbst wenn der Bucket MIME- und Größenrestriktionen hat. KI-Coding-Tools generieren standardmäßig nur clientseitige Validierung; das schützt nichts.

  1. Prüfe den MIME-Type serverseitig erneut anhand der tatsächlichen Bytes der Datei, nicht des Content-Type-Headers. Nutze eine Bibliothek wie file-type (Node) oder Magic-Byte-Sniffing. Ein Angreifer kann Content-Type: image/jpeg für eine Datei deklarieren, die in Wirklichkeit ein polyglotter HTML-/JavaScript-Payload ist.
  2. Entferne EXIF-Metadaten aus hochgeladenen Bildern. EXIF kann GPS-Koordinaten, Geräte-Seriennummern und Zeitstempel enthalten. Nutze sharp mit .withMetadata(false) oder exif-parser, um vor der Speicherung zu strippen.
  3. Lehne SVGs ab, die script-Tags oder onload-Handler enthalten. SVG ist XML — und viele KI-generierte Apps erlauben SVG-Uploads als „einfach ein Bild". Nutze DOMPurify serverseitig oder lehne SVG-Uploads ganz ab.
  4. Verwende deterministische, nicht erratbare Dateinamen. Behalte nicht den ursprünglichen Dateinamen. Nutze eine UUID oder einen Hash des Dateiinhalts. Ursprüngliche Dateinamen leaken ("passport_scan_2024_01_15.jpg") und vorhersehbare Namen ermöglichen Enumeration.

Signed URLs

Signed URLs sind der Weg, wie Clients auf private Buckets zugreifen. Die Ablaufzeit, der Bucket-Scope und was geloggt wird, sind wichtig.

  1. Setze die Signed-URL-Ablaufzeit standardmäßig auf 1 Stunde oder weniger. Die createSignedUrl(path, expiresIn)-Funktion des Supabase-JS-SDK nimmt Sekunden. Verwende niemals Werte wie 31536000 (ein Jahr) — die URL wird zu einem dauerhaft semi-öffentlichen Link.
  2. Speichere niemals Signed URLs in deiner Datenbank. Generiere bei jedem Request frische serverseitig. Eine gespeicherte Signed URL mit 1-Jahres-Ablaufzeit, die über einen DB-Dump leakt, gewährt Langzeitzugriff.
  3. Logge die Signed-URL-Generierung, nicht nur die Datei-Uploads. Wenn du später Kompromittierung vermutest, musst du wissen, wer welche URL wann generiert hat. Logge auth.uid() + Bucket + Objektpfad + Zeitstempel.
  4. Nutze die downloadAs-Option, wenn du nutzergenerierte Dateien ausspielst. createSignedUrl(path, expiresIn, { download: '.jpg' }) erzwingt einen Content-Disposition: attachment-Header, sodass die Datei heruntergeladen statt gerendert wird — schaltet die HTML-/SVG-/HTML-in-PDF-Ausführungsklasse aus.

Operative Hygiene

Die Storage-Konfiguration driftet über die Zeit. Diese vier operativen Punkte halten die Oberfläche eng.

  1. Auditiere Buckets quartalsweise. Dashboard → Storage → Buckets. Bestätige, dass Public/Private-Zustand und MIME-Type-Listen dem entsprechen, was die App erwartet. „Temporär" angelegte Buckets werden permanent, wenn niemand sie entfernt.
  2. Überwache anonyme Listen-Operationen. Storage-Logs (Dashboard → Logs → Storage) zeichnen LIST-Requests auf. Ein Spike anonymer Listen-Requests gegen einen privaten Bucket bedeutet, dass jemand ihn von außen sondiert.
  3. Setze eine Aufbewahrungsrichtlinie für ephemere Uploads. Temp-Buckets (Bildvorschau, Entwurfs-Uploads) sollten nach 24-72 Stunden via Scheduled Function automatisch gelöscht werden. Unbefristete Aufbewahrung ist eine Haftung unter den Datenminimierungs-Verpflichtungen der GDPR / CCPA.
  4. Lass einen FixVibe-Scan monatlich laufen. Der baas.supabase-storage-public-Check sondiert Buckets, die auf anonyme GET + LIST antworten. Neue Buckets kommen hinzu; alte ändern die Sichtbarkeit — nur kontinuierliches Scannen erfasst die Drift.

Nächste Schritte

Lass einen FixVibe-Scan gegen deine Produktions-URL laufen — anonyme Storage-Listings tauchen unter baas.supabase-storage-public auf. Kombiniere diese Checkliste mit Supabase-RLS-Scanner für die Tabellen-Ebene und Supabase-Service-Role-Key in JavaScript offengelegt für die angrenzende Key-Offenlegung. Für Storage-Fehlkonfigurationen über andere BaaS-Anbieter siehe BaaS-Fehlkonfigurations-Scanner.

// scanne deine baas-oberfläche

Finde die offene Tabelle, bevor es jemand anderes tut.

Gib eine Produktions-URL ein. FixVibe ermittelt die BaaS-Anbieter, mit denen deine App spricht, identifiziert ihre öffentlichen Endpunkte und meldet, was ein nicht authentifizierter Client lesen oder schreiben kann. Kostenlos, ohne Installation, ohne Karte.

  • Kostenloser Tarif — 3 Scans pro Monat, ohne Anmeldekarte.
  • Passives BaaS-Fingerprinting — keine Domain-Verifizierung erforderlich.
  • Supabase, Firebase, Clerk, Auth0, Appwrite und mehr.
  • KI-Fix-Prompts bei jedem Befund — füge sie zurück in Cursor / Claude Code ein.
Kostenlosen BaaS-Scan starten

keine anmeldung erforderlich

Supabase-Storage-Bucket-Sicherheits-Checkliste: 22 Punkte — Docs · FixVibe