Skip to content
VibeSafeDaily

Storage

Any signed-in user can overwrite or delete another user's uploads

Cataloged October 26, 2026 · Supabase · Lovable · Bolt · Cursor

Symptom

Avatars, documents, or receipts upload fine, and in the UI each person only sees their own. But the storage.objects policies only check which bucket a file is in. Any account that can sign up can replace another user's avatar, delete their files, or upload into their folder, using the same anon key and Storage API your app already calls.

Root cause

Supabase Storage keeps every file as a row in storage.objects, and access is ordinary row level security on that table. Supabase's access-control guide starts from a policy that only restricts the bucket (bucket_id = 'my_bucket_id') and then shows how to narrow it to a folder named after the user with (storage.foldername(name))[1]. AI-generated setups often stop at the first step and reuse the bucket-only check for the INSERT, UPDATE, and DELETE policies on the authenticated role. That makes every signed-in user an editor of every file in the bucket. Supabase's ownership guide says each object gets an owner_id from the uploader's JWT, but that ownership by itself does not provide any access control; a policy has to compare it. Making a bucket public does not change this: Supabase says public buckets skip access control only for reading, and uploading, deleting, moving, and copying are still decided by your policies. It also happens while debugging: overwriting a file with upsert needs SELECT and UPDATE policies, so a builder who hits an RLS error on re-upload often widens UPDATE to bucket_id only to make the error go away. A public example: the pathmind project's July 19, 2026 migration says its graphs bucket's write policies only checked bucket_id, so any authenticated user could upload, overwrite, or delete any file in the bucket, including other users' published presets. The fix scoped each policy to (storage.foldername(name))[1] = auth.uid()::text.

Before and after

The before policy lets any signed-in user change any file in the bucket. The after policy only allows changes inside the user's own folder.

Before

create policy "auth can update avatars"
on storage.objects for update to authenticated
using (bucket_id = 'avatars');

After

create policy "users update own avatars"
on storage.objects for update to authenticated
using (
  bucket_id = 'avatars'
  and (storage.foldername(name))[1] = (select auth.uid()::text)
)
with check (
  bucket_id = 'avatars'
  and (storage.foldername(name))[1] = (select auth.uid()::text)
);

Fix prompt

In this project only. Do not test against anyone else's app.

1. List every policy on storage.objects (in supabase/migrations, or ask me to run: select policyname, cmd, roles, qual, with_check from pg_policies where schemaname = 'storage' and tablename = 'objects';). For each one, write down the bucket, the command (SELECT, INSERT, UPDATE, DELETE, ALL), and whether it checks the user (auth.uid(), auth.jwt(), or owner_id) or only bucket_id.

2. Find every place the app uploads, replaces, moves, or deletes files (.upload(, .update(, .move(, .remove(, upsert: true) and tell me the path pattern each one uses.

3. For buckets that hold per-user files, make every upload path start with the user's id, for example `${user.id}/avatar.png`, taking the id from supabase.auth.getUser() on the client session, never from a form field.

4. Write a new migration that drops the bucket-only write policies and creates one policy per command, scoped to the owner:
   - INSERT to authenticated with check (bucket_id = '<bucket>' and (storage.foldername(name))[1] = (select auth.uid()::text))
   - UPDATE to authenticated with both using and with check set to that same expression
   - DELETE to authenticated using that same expression
   - SELECT to authenticated using that same expression, unless the bucket is meant to be readable by others (then say so and keep reads separate from writes)
   Do not use a single FOR ALL policy. Do not grant writes to anon.

5. If a bucket is shared by a team or org, check membership against my own table (for example a members table joined on the folder name) instead of the user id, and explain the rule before writing it.

6. Do not move these writes to the service role key to get around errors. If server code must write with the service role, it must check the caller's session and ownership itself first.

7. Tell me how to verify on my own app with two test accounts I own: as account A upload a throwaway file, then as account B try to overwrite and delete it, and confirm both fail and the file is unchanged.

Do not print keys, tokens, or user data. Do not edit unrelated files.

Paste this into the coding tool that has your own project open.

2-minute self-check

These steps are for an app you own. Do not run them against someone else's project.

  1. In your own Supabase project's SQL editor, run: select policyname, cmd, roles, qual, with_check from pg_policies where schemaname = 'storage' and tablename = 'objects'; Any INSERT, UPDATE, DELETE, or ALL policy whose qual and with_check mention only bucket_id (no auth.uid(), auth.jwt(), or owner_id) lets every signed-in user write to that bucket.
  2. With two test accounts you own, upload a throwaway file as account A. Sign in as account B in a private window and, in the browser console of your own app, try supabase.storage.from('<bucket>').upload('<A's path>', someFile, { upsert: true }) and then .remove(['<A's path>']). Then open the file in the dashboard as yourself. If it changed or is gone, the policy does not check ownership. Check the file itself, because a blocked delete may not come back as an error.
  3. Search your own code for .upload( and read the path it builds. If the path does not start with the user's id (for example uploads/<file name> or a timestamp), a per-user folder policy cannot protect it yet, and the fix has to change the path first.

Common questions

My avatars bucket is public. Doesn't that mean anyone can change the files anyway?

No. Public only changes reading: anyone with the URL can download the file. Supabase says uploading, deleting, moving, and copying in a public bucket are still controlled by your storage.objects policies. So a public avatars bucket can be safe to read and still need owner-scoped write policies.

Can I check owner_id instead of putting the user id in the path?

For reads, updates, and deletes, yes. Supabase's ownership guide shows owner_id = (select auth.uid()::text) in a delete policy. Files uploaded with the service key or from the dashboard have no owner, so they will not match any user. For uploads, a folder check on the path is the simplest rule, because the path is the part the client chooses.

Sources

Same symptom

Same platform