---
title: "Unsafe file uploads"
slug: unsafe-file-uploads
severity: high
category: Injection
summary: "An upload that trusts the browser's file type, keeps the original name, or serves the file from your site can become stored XSS or a malware host."
platforms:
  - Supabase
  - Firebase
  - Next.js
  - Vercel
updated: "2026-10-13"
related:
  - open-storage-buckets
  - xss
  - cors-wide-open
---

Upload bugs are a cluster, not one mistake.

The browser sends a filename and a content type. Both are chosen by the client. An agent often checks `file.type` in React and then stores whatever arrived. The server has to enforce size and type again.

SVG and HTML files are scripts if you serve them from your origin with a browser content type. A user uploads `avatar.svg`, you put it on your domain, and everyone who views the avatar runs the SVG.

Original filenames with `../` or a path can overwrite something else if you join them onto a directory. Generated names avoid that.

A public bucket is a separate bug. See [open storage buckets](/issues/open-storage-buckets). Even a private bucket is unsafe if the policy lets any signed-in user read every object.

## How to check in 5 minutes

1. Find the upload handler. Confirm size and type are checked on the server, not only in the file input's `accept` attribute.
2. See whether the stored name is the user's filename. If it is, that is a path and content-type problem.
3. Upload an SVG or HTML file to a test account you own and open the file URL. If it renders as a page on your domain, browsers will run script in it.
4. Check the storage policy or bucket flag. If anyone who can guess or list the URL can read other users' files, fix that with the open-bucket guide.

## How to fix

### Next.js

Limit size before you buffer the whole file. Allow a short list of types you actually need (for example PNG, JPEG, WebP). Detect type from the bytes if you can, and still do not trust the extension alone.

Store a generated name (`userId/random-id`), never the raw filename. Serve user files from a different host when you can, or with `Content-Disposition: attachment` and `X-Content-Type-Options: nosniff`. Do not serve user SVG or HTML as an image preview on your origin.

### Supabase

Use a private bucket and storage policies that mirror table RLS: a user can upload and read objects under their own prefix. Reject other prefixes in the policy. Supabase storage is not covered by a table's RLS. The storage policy is the control.

Set a file size limit in the bucket settings and again in the route that creates the signed upload if you have one.

### Firebase

Storage rules should require `request.auth.uid` to match the path, limit `request.resource.size`, and check `request.resource.contentType` against your list. `allow read, write: if true` is the open-bucket bug.

### Vercel

If you use Vercel Blob or similar, the same rules apply: private by default, random names, size cap, and do not redirect users to a same-site HTML upload. Check that product's current docs for the exact ACL field. The principle does not depend on the vendor.

## Agent prompt

```text
You are tightening file uploads. Do not add features.

Find every upload path, including direct-to-bucket uploads.

1. Enforce a maximum size on the server or in the storage policy.
2. Allow only the content types this product needs. Do not allow SVG or HTML unless I said the product needs them, and do not serve those from this site's origin.
3. Store a generated object name. Do not use the user-supplied filename as the path.
4. Make the bucket private. Add a read policy so a user can only read their own prefix. Do not leave the bucket public to make previews easy. Use a short-lived signed URL instead.
5. Set nosniff on responses you control.

Show the policy or handler you changed and how a second user is blocked from reading the file.
```

## FAQ

### The input accept attribute is image/*. Is that the check?

No. That hint is for the file dialog. The request can send any bytes. Check on the server or in storage rules.

### Can I rename the file to .png and be done?

No. The bytes and the content type you send when someone downloads it decide how the browser treats it. A script renamed to `.png` and served as `text/html` is still HTML.

## Sources

- [OWASP File Upload Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html)
