Ask a non-technical founder what worries them about their AI-built app, and they’ll usually say payments, or logins, or whether the database is backed up. Almost nobody says file uploads. That’s the problem. File upload security is the thing we check first when we audit a vibe-coded app, because it’s the feature most likely to have been generated in thirty seconds flat, approved because “it worked when I tested it,” and never looked at again. A profile picture field. A document upload for a client portal. A resume uploader for a hiring tool. Each one is a door into your storage, and in most of the apps we see, that door was never actually locked.
We’re not exaggerating when we say this is the single most under-checked risk category in AI-generated code. Payments get scrutiny because money is involved. Authentication gets scrutiny because everyone’s heard of password leaks. Uploads get built, demoed once with a test image, and shipped.
Why uploads are riskier than they look
An upload feature looks simple from the outside. User picks a file, file goes somewhere, user sees a success message. But underneath that one interaction, you’re making several decisions at once: who’s allowed to upload, what they’re allowed to upload, where it gets stored, who can read it back, and what happens if someone sends something malicious instead of a holiday photo.
AI coding tools are very good at the happy path. Ask for “an upload feature for profile photos” and you’ll get a working file input, a storage call, and a confirmation message, all in a tidy response. What you won’t get, unless you ask explicitly and then verify the answer, is validation of file type, a sane size limit, or an access policy that actually restricts who can fetch the file afterwards. The AI assumed you wanted the feature to work. It didn’t assume you wanted it to be safe, because you didn’t say so, and most founders don’t know that’s a separate ask.
This connects to a wider pattern we cover in our vibe code security guide: AI-generated code tends to satisfy the literal instruction and stop there. Security is implicit in a human developer’s training. It isn’t implicit in a prompt.
The mistakes we find
Four problems show up again and again, in roughly this order of frequency.
The first is public storage buckets. Supabase and Firebase both make it trivially easy to spin up a storage bucket, and both default (or nearly default, depending on the tool) to permissive access during development. The app ships, the bucket stays public, and anyone with the base URL can browse it. We’ve found entire folders of uploaded ID documents, invoices, and private photos sitting open with no authentication at all. If you’ve read our piece on Supabase mistakes in AI-generated code, this will sound familiar, because storage bucket policies are one of the most commonly skipped steps in that whole platform.
The second is missing file-type checks. The upload form might say “images only,” but that’s a frontend hint, not a security control. Without server-side validation, a user can upload a PHP script, an HTML file with embedded JavaScript, or an executable disguised with an image extension. Depending on how the file is later served, that can turn into stored cross-site scripting or, in the worst cases, remote code execution on the server.
The third is no size limits. An uploader with no cap will happily accept a 4GB file, and an attacker can script a loop that submits dozens of them in sequence. That’s a cheap way to exhaust your storage quota or run up your cloud bill, and it needs no cleverness at all, just patience and a script.
The fourth is guessable URLs to private files. Even when a bucket itself isn’t public, we often find files named by their original filename or a sequential ID, with the “privacy” resting entirely on the assumption that nobody will guess the URL. That’s not access control. Anyone who can enumerate or guess a pattern, invoice-1.pdf, invoice-2.pdf, and so on, can read files that were never meant to be theirs.
What attackers do with each
None of this is theoretical. A public bucket gets found through automated scanners that crawl common storage URL patterns and index whatever they can read, no targeting required. Missing file-type validation gets exploited by uploading a crafted file that executes when a victim views it, which is how stored XSS attacks usually start. Missing size limits get used for cheap denial-of-service, inflating your bill until the owner notices something’s wrong, usually from a billing alert rather than a security alert. And guessable URLs get walked systematically. We’ve seen scripts that just increment an ID and save whatever comes back, pulling an entire customer document archive in minutes.
The common thread is that none of these attacks require skill. They require that the attacker tried the obvious thing, and the obvious thing worked. That’s a different risk profile from, say, exposed API keys, which at least require someone to go looking through your source. An open storage bucket just needs to be found.
A self-check for your upload feature
You don’t need to read code to check most of this yourself. Open your app, find every place a user can upload a file, and work through it.
Try uploading a file with the wrong extension, a .exe renamed to .jpg, or a text file where an image is expected. If it uploads without complaint, you have no server-side type checking.
Try uploading something large, a few hundred megabytes if you can find a test file that size. If it goes through without a visible limit, there isn’t one.
Find the URL of a file you’ve uploaded and change a character or a number in it. If you can still load a different file, or load the same file while logged out, your access controls aren’t doing their job.
If your app uses Supabase or Firebase, log into the dashboard and check the storage bucket’s access rules directly. “Public” should be the exception you chose on purpose, not the default you never looked at.
What a safe setup looks like
A properly built upload feature validates file type and size on the server, not just in the browser form. It stores files in a bucket that’s private by default, with access granted per-file through short-lived signed URLs rather than a permanently public path. File names get randomised or hashed on the way in, so nobody can guess one file’s URL from another’s. And there’s a reasonable upper limit on both individual file size and total storage per user, so a single bad actor or a single bug can’t run up an unbounded bill.
None of this is exotic engineering. It’s maybe twenty extra lines of configuration and a couple of policy rules. The reason it’s missing from so many AI-built apps isn’t difficulty, it’s that nobody asked for it, and the AI had no reason to volunteer it.
Letting users upload files? Let us check nobody else can read them. Free. Get in touch and we’ll run a proper look at your upload setup before it becomes someone else’s discovery.