HIGH Supabase Storage · S3 · Cloudinary · local disk

File uploads with no type, size or ownership checks

What you'd see: Your upload handler takes file.name and file.type and saves the file.

§ 01 — What's actually happening

Both of those values come from the client and both can be set to anything. A file called <code>photo.png</code> can contain a PHP script; a file claiming <code>image/jpeg</code> can be a 4 GB zip. If the destination is publicly served and executable, that is a very short path to running code on your server.

Why an AI tool writes it this way

"Let users upload a profile picture" describes the happy path, and the happy path is what gets built. The <code>accept="image/*"</code> attribute on the input looks like validation, but it only filters the file picker dialog.

What it costs you

Ranges from storage bills and broken pages to remote code execution on hosts that run PHP from the upload directory. Even without execution, an unbounded upload endpoint is a free file host — and you pay the bandwidth.

§ 02 — Before & after

The upload box that accepts anything

Illustrative code, written for this page — never a client's project.

AI-generated

  • Trusts the client-supplied MIME type and extension
  • No size limit, so one request can fill the disk
  • Original filename used as the path — ../ included
const file = formData.get('avatar'); await supabase.storage .from('public-uploads') .upload(file.name, file); // their name, their type, any size await db.user.update({ where: { id: user.id }, data: { avatarUrl: `/public-uploads/${file.name}` } });

Human-reviewed

  • Type confirmed by reading the file's magic bytes
  • Hard size limit enforced before anything is written
  • Random filename, correct extension, stored outside the executable path
const file = formData.get('avatar'); const MAX = 5 * 1024 * 1024; if (!file || file.size === 0) return bad('No file received'); if (file.size > MAX) return bad('Images must be under 5 MB'); // Check what the file actually is, not what it claims to be const head = new Uint8Array(await file.slice(0, 12).arrayBuffer()); const kind = sniffImage(head); // png / jpeg / webp / gif, else null if (!kind) return bad('Only PNG, JPEG, WebP or GIF images are accepted'); const key = `avatars/${user.id}/${crypto.randomUUID()}.${kind}`; await supabase.storage.from('user-uploads').upload(key, file, { contentType: `image/${kind}`, upsert: false });

§ 03 — Check your own

How to tell in two minutes

You don't need us to run these. If any of them come back the wrong way, you have this problem.

  1. Rename a text file to test.png and upload it. If it is accepted, only the extension is being checked.
  2. Try a large file — 100 MB or so. A server that accepts it will accept a thousand of them.
  3. Look at where files land. If uploads sit in a folder your web server executes, that is the serious version of this bug.
  4. Check the stored path includes the user ID or a random component. Predictable paths let one user overwrite another's file.

Found it in your project? Fixing this one properly usually takes an engineer under an hour. Finding the other nine takes longer — which is what the free health check is for.

Get my free health check

Find out where you actually stand.

Free. No obligation. Reviewed by experienced engineers.

Get my free health check