RowShield
Guides

A Supabase storage audit you can run in ten minutes

Storage is the quiet corner of most Supabase projects: tables get policies during setup, buckets inherit whatever the first upload needed, and nobody revisits either. Yet the failure modes are few and mechanical — a bucket flipped to public, an always-true policy on storage.objects, or Row Level Security switched off on the objects table itself.

The checklist below covers all three in about ten minutes with queries you can run today, and each step maps onto a check RowShield runs continuously afterwards.

Rules that check this

List every bucket and its visibility

Start with inventory, because you cannot secure what you cannot name. The buckets table records one row per bucket with the public flag that decides whether objects are served to anyone holding a URL. Run it in the SQL editor and read it as a publication list, because that is what it is:

SELECT id, name, public, file_size_limit, allowed_mime_types
FROM storage.buckets
ORDER BY name;

Check who can read and write object metadata

Bucket visibility is only half the surface. Access to objects also flows through policies on storage.objects, so a private bucket can still be wide open if that table carries an always-true policy for anon. Two checks matter: that RLS is enabled on storage.objects at all, and that no permissive policy grants anonymous writes — an anonymous-write hole is worse than a public read bucket, because it lets anyone host arbitrary content under your project:

SELECT policyname, cmd, roles, permissive, qual, with_check
FROM pg_policies
WHERE schemaname = 'storage'
  AND tablename = 'objects'
ORDER BY policyname;

-- And confirm the flag itself:
SELECT relrowsecurity AS rls_enabled
FROM pg_class
WHERE oid = 'storage.objects'::regclass;

Probe the public surface directly

For any bucket flagged public, behave like the internet. A HEAD request against the documented public-object path either resolves or does not, and object paths are routinely guessable from filenames or sequential identifiers, so treat existence as published. Keep your own probing read-only — there is no version of this test that needs to write:

curl -I "https://your-project-ref.supabase.co/storage/v1/object/public/reports/2024-summary.pdf"

Fixing what you find

Close the bucket, then re-grant deliberately. Flipping the flag makes objects reachable only through signed URLs or through policies you write; the owner-scoped pattern below lets authenticated users read from their own first-level folder, which matches how most projects were informally organised anyway. If storage.objects had RLS disabled, enable it first — until that flag is on, no policy you write means anything:

UPDATE storage.buckets SET public = false WHERE id = 'reports';

CREATE POLICY "reports_read_own"
  ON storage.objects
  FOR SELECT
  TO authenticated
  USING (
    bucket_id = 'reports'
    AND (storage.foldername(name))[1] = ((SELECT auth.uid())::text)
  );

Keeping it checked

Every step above reads metadata rather than object contents, which is why it automates cleanly. On each scheduled scan RowShield reads bucket flags and the storage.objects posture, reports every public bucket as a finding with the remediation SQL generated using your real bucket ids, escalates to critical when an anonymous-write policy is also open, and separately flags storage.objects running with RLS off. Alerts fire on transitions — a bucket flipping public after Friday's deploy is precisely the event worth a message — and drift diffing shows the change as a change. RowShield is an independent product, not affiliated with or endorsed by Supabase; it issues GET, HEAD and OPTIONS requests only, and never stores object data. Connect a project at rowshield.dev/audit — the free tier includes daily scans.

Frequently asked

Are public buckets ever the right choice?
Yes — genuinely published assets such as documentation files or marketing images belong in a public bucket, because signed URLs would add cost and complexity for zero protection. The failure is not publicity itself but publicity nobody decided: the checklist exists so every public flag is a decision.
Will making a bucket private break existing links?
Yes. Public URLs stop resolving the moment the flag flips, so migrate consumers to signed URLs or policy-gated downloads as part of the same change. Anything cached by third parties keeps its copy regardless — privacy was never a property those links had.
Can I check my storage without connecting the database?
Partly. The free URL probe needs only your project address and tests table exposure with the anon key; bucket flags and storage.objects policies live in catalog metadata, which requires connecting the project. Both paths are read-only by construction.

Check your project in about ten seconds

Paste a URL. No signup, no writes, nothing stored.

Run the free audit
supabase storage security auditsupabase public bucket checkstorage.objects rls policy