FixVibe

// docs / baas security / supabase storage

Supabase storage bucket security checklist: 22 items

Supabase Storage is a thin wrapper around an S3-compatible bucket plus the same Row-Level Security model as the database. That means the same RLS pitfalls that affect tables affect file access โ€” and a few storage-specific ones that show up when AI coding tools wire up uploads. This checklist is 22 items across five sections: bucket configuration, RLS policies, upload validation, signed URLs, and operational hygiene. Each is verifiable in under 15 minutes.

Each item below is essential. For the underlying RLS mechanics, see Supabase RLS scanner. For the key-exposure class adjacent to storage, see Supabase service role key exposed in JavaScript.

Bucket configuration

Start with the right defaults. A misconfigured bucket leaks files whether your RLS is correct or not.

  1. Default every bucket to private. In the Supabase Dashboard โ†’ Storage โ†’ Buckets, set the Public bucket toggle to off unless you have an explicit reason (marketing assets, public avatars with no PII). Public buckets bypass RLS for read operations โ€” anyone with the bucket name can list and download.
  2. Set a hard file size limit on every bucket. Dashboard โ†’ Bucket settings โ†’ File size limit. 50 MB is a sensible default for user uploads; raise it deliberately for video / large-file use cases. Without a limit, a single malicious upload can exhaust your storage quota or your monthly bandwidth.
  3. Restrict allowed MIME types per bucket. Allowed MIME types list โ€” explicit allowlist, not blocklist. image/jpeg, image/png, image/webp for image-only buckets. Never allow text/html, application/javascript, or image/svg+xml in a user-content bucket โ€” they execute in the browser when served via signed URL.
  4. Use one bucket per content type, not one shared bucket. Per-bucket settings (size, MIME types, RLS policies) are the granularity you have. A user-avatars bucket, a document-uploads bucket, and a public-assets bucket are easier to lock down than one mixed bucket.
  5. Verify CORS configuration if frontend uploads. If users upload directly from the browser to a signed URL, the bucket CORS must list your production origin. * is acceptable for public buckets only โ€” never for buckets containing user PII.

RLS policies on storage.objects

Supabase Storage stores file metadata in the storage.objects table. RLS on that table controls who can read, upload, update, or delete files. Without RLS, the bucket's public/private flag is your only protection.

  1. Confirm RLS is enabled on storage.objects. SELECT rowsecurity FROM pg_tables WHERE schemaname = 'storage' AND tablename = 'objects'; must return true. Supabase enables it by default on new projects; verify it has not been disabled.
  2. Write a SELECT policy scoped to auth.uid() for private buckets. CREATE POLICY "users_read_own_files" ON storage.objects FOR SELECT USING (auth.uid()::text = (storage.foldername(name))[1]);. The convention is to store files under [user-id]/[filename] and use storage.foldername() to extract the owner from the path.
  3. Write an INSERT policy that enforces the same path convention. CREATE POLICY "users_upload_own" ON storage.objects FOR INSERT WITH CHECK (auth.uid()::text = (storage.foldername(name))[1]);. Without WITH CHECK, an authenticated user can upload into another user's folder.
  4. Add UPDATE and DELETE policies if your app supports file edits or deletes. Each command needs its own policy. Skipping DELETE means authenticated users cannot remove their own files; skipping UPDATE means file overwrites silently fail.
  5. Test cross-user access in two browser sessions. Sign in as User A, upload a file, copy the path. Sign in as User B in another browser, try to fetch the file via the REST API. The response must be 403 or 404, never 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 validation

Validate every upload server-side, even when the bucket has MIME and size constraints. AI coding tools generate client-only validation by default; that protects nothing.

  1. Re-check MIME type server-side from the file's actual bytes, not the Content-Type header. Use a library like file-type (Node) or magic-byte sniffing. An attacker can claim Content-Type: image/jpeg on a file that is actually a polyglot HTML / JavaScript payload.
  2. Strip EXIF metadata from uploaded images. EXIF can contain GPS coordinates, device serial numbers, and timestamps. Use sharp with .withMetadata(false) or exif-parser to strip before storage.
  3. Reject SVGs that contain script tags or onload handlers. SVG is XML โ€” and many AI-generated apps allow SVG uploads as "just an image." Use DOMPurify server-side or refuse SVG uploads entirely.
  4. Use deterministic, unguessable filenames. Don't preserve the original filename. Use a UUID or a hash of the file contents. Original filenames leak ("passport_scan_2024_01_15.jpg") and predictable names enable enumeration.

Signed URLs

Signed URLs are how clients access private buckets. The expiry, the bucket scope, and what gets logged matter.

  1. Default signed-URL expiry to 1 hour or less. The Supabase JS SDK's createSignedUrl(path, expiresIn) takes seconds. Never use values like 31536000 (one year) โ€” the URL becomes a permanent semi-public link.
  2. Never store signed URLs in your database. Generate fresh ones server-side on every request. A stored signed URL with a 1-year expiry that leaks via a database dump grants long-term access.
  3. Log signed-URL generation, not just file uploads. If you suspect a compromise later, you need to know who generated which URL when. Log auth.uid() + bucket + object path + timestamp.
  4. Use the downloadAs option when serving user-uploaded files. createSignedUrl(path, expiresIn, { download: '.jpg' }) forces a Content-Disposition: attachment header so the file downloads instead of rendering โ€” defeats the HTML / SVG / HTML-in-PDF execution class.

Operational hygiene

Storage configuration drifts over time. These four operational items keep the surface tight.

  1. Audit buckets quarterly. Dashboard โ†’ Storage โ†’ Buckets. Confirm public/private state and MIME-type lists match what the app expects. Buckets created "temporarily" become permanent if no one removes them.
  2. Monitor anonymous list operations. Storage logs (Dashboard โ†’ Logs โ†’ Storage) record LIST requests. A spike of anonymous list requests against a private bucket means someone is probing it from the outside.
  3. Set a retention policy for ephemeral uploads. Temp buckets (image preview, draft uploads) should auto-delete after 24-72 hours via a scheduled function. Indefinite retention is a liability under GDPR / CCPA data-minimisation obligations.
  4. Run a FixVibe scan monthly. The baas.supabase-storage-public check probes for buckets that respond to anonymous GET + LIST. New buckets get added; old ones change visibility โ€” only continuous scanning catches the drift.

Next steps

Run a FixVibe scan against your production URL โ€” anonymous storage listings show up under baas.supabase-storage-public. Pair this checklist with Supabase RLS scanner for the table layer and Supabase service role key exposed in JavaScript for the key-exposure adjacency. For storage misconfigurations across other BaaS providers, see BaaS misconfiguration scanner.

// scan your baas surface

Find the open table before someone else does.

Drop in a production URL. FixVibe enumerates the BaaS providers your app talks to, fingerprints their public endpoints, and reports what an unauthenticated client can read or write. Free, no install, no card.

  • Free tier โ€” 3 scans / month, no signup card.
  • Passive BaaS fingerprinting โ€” no domain verification needed.
  • Supabase, Firebase, Clerk, Auth0, Appwrite, and more.
  • AI fix prompts on every finding โ€” paste back into Cursor / Claude Code.
Supabase storage bucket security checklist: 22 items โ€” Docs ยท FixVibe