Skip to content

Is It Safe to Let Users Upload Files to My App?

6 min read

Letting users upload files is safe only if you validate every upload on the server: check the file type, cap the size, store it away from your app's code, and serve it so it can never run as a script. A raw "upload a file" feature with none of those checks is one of the easier things for a stranger to abuse, and most vibe-coded versions ship with none of them.

Why this happens

You asked the agent for "let users upload a profile picture" and it gave you exactly that: a file input, an upload call, a URL saved to the database. It works in the demo because you uploaded a normal photo. What you didn't ask for, and didn't get, is everything that handles a user who is not being polite: a 20 MB file, a .php or .html file renamed to .jpg, ten thousand uploads in a minute, or a filename crafted to overwrite something else.

The agent built the happy path. Uploads are almost entirely about the unhappy path, and that part is invisible until a real stranger goes looking for it.

How to check

Try to be your own attacker for five minutes.

  1. Upload something that isn't an image. Rename a .txt or .html file to .jpg and upload it. If the app accepts it, your type check is trusting the file extension, which anyone can change.
  2. Upload something huge. Grab a 50 MB file and try. If it goes through with no error, you have no size limit, and your storage bill is now up to your users.
  3. Open the file's URL directly in a fresh browser tab. Is it a long random path, or a guessable one like /uploads/1.jpg, /uploads/2.jpg? Guessable URLs mean anyone can walk through other people's files.
  4. Check where it's stored. If uploads land in the same folder your app runs from, an uploaded script could be requested and executed by the server. That is the worst case.

The fix

  1. Validate on the server, never only in the browser. Client-side checks are for user convenience; an attacker skips your UI entirely and calls the upload endpoint directly. Re-check type and size in your backend or storage rules.
  2. Check the file's real content, not its name. Verify the actual MIME type or magic bytes, and keep an allow-list of exactly the types you need (for example image/png, image/jpeg). Reject everything else.
  3. Set a hard size limit on the server and, where possible, at the storage layer. On Supabase Storage and Firebase Storage you can cap size and type in the bucket's security rules so the limit holds even if your code is bypassed.
  4. Store uploads outside your app's web root, ideally in dedicated object storage (Supabase Storage, Firebase Storage, S3). Never save user files into the folder your server executes code from.
  5. Serve files so they can't run. Give them randomised names, and return a Content-Disposition or content-type that forces download or renders as plain data, so an uploaded .html or .svg can't execute in a visitor's browser.
  6. Tie each file to its owner and gate access with the same rules that protect your database, so one user can't read another's uploads by guessing a URL.

The trap to avoid

Do not rely on the file extension or the browser's accept attribute as your security. Both are trivially bypassed: an attacker renames the file and posts straight to your endpoint. Checking filename.endsWith(".jpg") feels like a validation, but it stops no one who is actually trying.

Where this fits

An upload box is a door you opened into your own storage, and strangers will test its hinges before your friends do. The free Readiness Report checks whether your upload paths validate type and size on the server and whether your stored files are actually locked to their owners, the same way it checks that nobody can read your database from the browser or swap a user ID to see someone else's data. If you'd rather have the gaps closed for you in order, that is what the Finishing Pass is for. Uncapped uploads are also a storage-and-cost abuse problem, not only a security one.