User uploads publicly accessible: how to fix it
User uploads are publicly accessible when they sit in a public bucket or when a public URL works without a login, so make the bucket private and serve each file through a short-lived signed URL. A public bucket has no read check. Anyone who has the file URL can download the file, with no account. This guide shows how to confirm the problem, fix it, and check who read your files.
The guide covers the three storage types that AI app builders use most: Supabase Storage, Amazon S3, and files on your own server. The fix has the same shape for each type. Remove the public read access, then give each user a temporary link only after your server checks the user.
What does “user uploads publicly accessible” mean?
It means that a person who is not logged in can open a file that a user uploaded. The person only needs the URL. Examples are invoices, ID scans, medical notes, private photos, contracts, and exported reports.
The cause is almost always a storage setting, not a bug in your code. The login protects the pages of the app. It does not protect the storage service, which has its own URLs and rules.
The Supabase storage documentation describes the two bucket types. A private bucket checks access with RLS policies for all operations, including downloads. A public bucket skips the access check for reads. The page says that anyone who has the asset URL can open the file. Supabase makes buckets private by default, so a public bucket is a choice that someone made.
Amazon S3 works in a similar way. AWS says that new buckets and objects do not allow public access by default. A user can change a bucket policy or an object permission and make the data public.
Three problems appear together in most cases:
| Problem | Effect |
|---|---|
| The bucket is public | Anyone who has a URL can read the file |
| The file names are easy to guess | A person can try many names and find other files |
| The bucket allows listing | A person can read all the file names at once |
You must fix the first problem. The other two make the damage larger. A private bucket with signed URLs removes all three.
For the full policy guide, read the Supabase RLS guide.
How do I check if my uploads are public?
Use a browser session with no login. Do the checks in this order.
- Upload a test file. Log in to your app. Upload a file that has no real data, for example a small image.
- Copy the file URL. Right-click the file in the app and copy the image address, or open DevTools and read the Network tab.
- Open a private window. A private window has no cookies. Paste the URL.
- Read the result. If the file opens, the file is public. If you see a 400 or 403 error, the file is protected.
- Test the URL from a terminal. A terminal has no cookies, no token, and no session.
curl -sI "https://YOUR-PROJECT.supabase.co/storage/v1/object/public/uploads/user-1/invoice.pdf"
A 200 status means anyone can download the file. Replace the host and the path with your own values.
Look at the URL itself. In Supabase, the path /storage/v1/object/public/ shows that the file comes from a public bucket. In S3, a URL of the form https://BUCKET.s3.REGION.amazonaws.com/KEY that opens without a query string such as X-Amz-Signature shows a public object.
Then check the settings of the bucket:
- Supabase. Open the Storage page in the dashboard. A public bucket shows a public label. You can also read the setting with SQL:
select id, name, public from storage.buckets;
- S3. Open the bucket in the AWS console and select the Permissions tab. Read the Block public access section. If any of the four settings is off, read the bucket policy and the ACLs.
You can also check the AWS status from a terminal:
aws s3api get-bucket-policy-status --bucket YOUR-BUCKET
The GetBucketPolicyStatus operation is in the AWS bucket-level documentation. Its result tells you if the bucket policy is public.
Warning: Do not test with a real user file.
What do I do first? Fix it now, in order of urgency
Do the steps in this order. Steps 1 and 2 stop new reads. The other steps make the fix last.
Caution: When you make a bucket private, the old public URLs stop working. Images in your app can break until you deploy the new code. This is the correct result. A broken image costs less than a leaked document.
- Decide which files are public on purpose. Examples are a logo or a blog image. Write the list. All other files are private.
- Make the bucket private. In Supabase, open Storage, edit the bucket, and turn off the public setting. You can also use SQL:
update storage.buckets set public = false where id = 'uploads';
- Block public access in S3. Turn on all four Block Public Access settings for the bucket. AWS recommends this in the Block Public Access guide. The CLI command is:
aws s3api put-public-access-block --bucket YOUR-BUCKET \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
AWS also says that these settings do not change the existing policies. If you remove a setting later, the old public policy works again. Delete any bucket policy that has "Principal": "*" for s3:GetObject.
- Move private files out of public buckets. Create a second, private bucket. Copy the private files to it and update the paths in your database.
- Add storage policies for each user. In Supabase, a private bucket needs an RLS policy before a user can read a file. The Supabase access control page shows a policy of this form:
create policy "Individual user Access"
on storage.objects for select
to authenticated
using ( (select auth.jwt()->>'sub') = owner_id );
This policy lets a user read the files that the user uploaded. The page also warns that, without the correct helper functions, users can list the contents of a bucket. Test listing after you add the policy.
- Serve files with signed URLs. See the next section.
- Change the old file names. The old public URLs can be in emails, chats, caches, and search results. A new file name makes the old URL useless. Do this for sensitive files.
- Deploy and test again. Repeat the check from the previous section. The public URL must now fail.
How do I give users access to private files?
Use a signed URL. A signed URL is a normal URL that has a signature and an expiry time. Only your server can create it, because the signature needs a secret. The storage service checks the signature and the time, then returns the file.
In Supabase, call createSignedUrl. The reference states that expiresIn is a number of seconds:
const { data, error } = await supabase.storage
.from("uploads")
.createSignedUrl(`${user.id}/invoice.pdf`, 60);
if (error) throw error;
return data.signedUrl;
The example creates a link that works for 60 seconds. When the browser uses the client with the user session, the storage RLS policy decides if the user can read the file. This is the safe design. Do not create signed URLs with the service_role key for any path that a user sends. If you must use that key, read what to do about a service_role key first, and check the path in your own code.
In S3, create a presigned URL on the server. The AWS CLI example from the reference is:
aws s3 presign s3://YOUR-BUCKET/user-1/invoice.pdf --expires-in 60
In an application, use the presign function of your AWS SDK. The default expiry of the CLI command is 3600 seconds. The maximum is 604800 seconds, which is one week.
Follow these rules for signed URLs:
| Rule | Reason |
|---|---|
| Create the URL after your server checks the user | The URL gives access to every person who holds it |
| Use a short expiry time | A leaked URL stops working fast |
| Create the URL when the user opens the page | Do not store the URL in your database |
AWS describes presigned URLs as bearer tokens. For this reason, do not write signed URLs into logs or error messages.
Warning: The path must come from your database, not from the request. If the browser sends ?path=user-2/passport.png and your server signs it, you create an access control problem. This is the same fault as a broken object check. See BOLA in AI-generated CRUD.
Files on your own server
If your app saves files in a folder that the web server serves, every file in the folder is public. The OWASP File Upload cheat sheet gives these rules:
- Store the files on a different server. If you cannot, store them outside the web root.
- Use a random name that the application generates, such as a UUID.
- Serve the file through a handler in the application. The handler maps an internal ID to the file name.
The handler is the place to check the user. Find the owner in the database, compare the owner with the session user, and then stream the file.
How do I check the damage?
Read the access logs for the period in which the files were public. You want to know if someone other than the owner read a file.
- Find the period. Look at the history of the bucket setting, the git history, and the deploy history. Find the first day that the bucket was public.
- Read the storage logs. Open the storage or API logs of your project and look for reads of the public path. For S3, check if server access logging or CloudTrail data events were on before the problem. If not, you have no record of the reads.
- Look for unknown clients. Look for IP addresses and user agents that your users do not use. Look for many requests in a short time.
- Look for listing calls. A list request shows that someone read many file names at once. Treat every file in the listed folder as read.
- Rank the files. A bucket with invoices and ID scans has a higher risk than a bucket with product photos.
- Check other buckets. Run the SQL query for all buckets. One public bucket often means that a second one exists.
If you have no logs, assume that the files were read. Decide with a lawyer or a security engineer if you must tell your users. The rules depend on the country and on the type of data.
Note: Logs do not show if a person made a copy. Treat a file that was public as copied.
How do I stop public uploads in the future?
Make private the default. Then add checks that find a mistake before the users do.
- Create new buckets as private. Make a public bucket only for files that every visitor may see. Name it so that the name shows the purpose, for example
public-assets. - Keep user files under the user ID. Use a path of the form
user_id/file_name. A simple path makes the policy simple. - Write one policy for each operation. Write a policy for read, one for upload, one for update, and one for delete. Do not use one policy that allows all operations.
- Limit the upload itself. Set a maximum size and a list of allowed types in your server code. See file upload risks.
- Block public access at the account level in AWS. The AWS guide recommends that you turn on all four settings for the account. Then a new bucket does not start as public.
- Review AI-generated storage code. Search each change for
public: true,getPublicUrl,"Principal": "*", andacl: "public-read". Add a rule to your coding tool that says to never make a bucket public for user data. - Add a test. Write a test that uploads a file, reads the URL with no login, and expects a 400 or 403 error. Run it after every deploy.
- Scan the deployed app. A scan of the live site finds file URLs that the browser really receives. It finds problems that the source code does not show.
If your app serves files on a custom domain, read the S3 and subdomain takeover pattern.
When should I get help?
Get help in these cases:
- The files hold personal data, health data, payment data, or ID documents. Ask a security engineer to review the app. You may have legal duties to report the incident.
- The same bucket also holds backups, database dumps, or
.envfiles. Delete each secret in it and rotate it.
After you finish, run the first check again. Open a private window, paste a file URL, and confirm that the browser shows an error.
Common questions
Is a long random file name enough to protect an upload?
Do I need a signed URL for profile pictures?
Does a signed URL stop a user from sharing the file?
Does making the bucket private break the existing links?
Can I make only one file private in a public bucket?
Find public uploads
A VibeEval scan finds the file URLs that your app shows to the browser and reports files that open without a login.
14-day free trial · No credit card · Cancel anytime