// 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.
- 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.
- 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.
- Beschränke erlaubte MIME-Types pro Bucket. Erlaubte-MIME-Types-Liste — explizite Allowlist, keine Blocklist.
image/jpeg,image/png,image/webpfür Bild-only-Buckets. Erlaube nietext/html,application/javascriptoderimage/svg+xmlin einem Nutzerinhalt-Bucket — sie führen im Browser aus, wenn sie via Signed URL ausgeliefert werden. - 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, eindocument-uploads-Bucket und einpublic-assets-Bucket sind einfacher zu sperren als ein gemischter Bucket. - 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.
- Bestätige, dass RLS auf storage.objects aktiviert ist.
SELECT rowsecurity FROM pg_tables WHERE schemaname = 'storage' AND tablename = 'objects';musstrueliefern. Supabase aktiviert es standardmäßig bei neuen Projekten; verifiziere, dass es nicht deaktiviert wurde. - 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 nutzestorage.foldername(), um den Eigentümer aus dem Pfad zu extrahieren. - 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. - 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.
- 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
403oder404sein, niemals200.
-- 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.
- Prüfe den MIME-Type serverseitig erneut anhand der tatsächlichen Bytes der Datei, nicht des
Content-Type-Headers. Nutze eine Bibliothek wiefile-type(Node) oder Magic-Byte-Sniffing. Ein Angreifer kannContent-Type: image/jpegfür eine Datei deklarieren, die in Wirklichkeit ein polyglotter HTML-/JavaScript-Payload ist. - Entferne EXIF-Metadaten aus hochgeladenen Bildern. EXIF kann GPS-Koordinaten, Geräte-Seriennummern und Zeitstempel enthalten. Nutze
sharpmit.withMetadata(false)oderexif-parser, um vor der Speicherung zu strippen. - Lehne SVGs ab, die
script-Tags oderonload-Handler enthalten. SVG ist XML — und viele KI-generierte Apps erlauben SVG-Uploads als „einfach ein Bild". NutzeDOMPurifyserverseitig oder lehne SVG-Uploads ganz ab. - 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.
- 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 wie31536000(ein Jahr) — die URL wird zu einem dauerhaft semi-öffentlichen Link. - 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.
- 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. - Nutze die
downloadAs-Option, wenn du nutzergenerierte Dateien ausspielst.createSignedUrl(path, expiresIn, { download: '.jpg' })erzwingt einenContent-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.
- 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.
- Ü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. - 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.
- Lass einen FixVibe-Scan monatlich laufen. Der
baas.supabase-storage-public-Check sondiert Buckets, die auf anonymeGET+LISTantworten. 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.
