File upload accepts any file type: how to find the cause and fix it
To fix an upload that accepts any file type, check the type, the size, and the name of every file on the server, and reject all files that are not on your allow-list. The accept attribute in the HTML form does not do this work. It only filters the file picker. An attacker sends the request without your form.
This guide shows how to confirm the problem, how to fix it in order of urgency, and how to check for files that someone already uploaded. It also gives an example for Supabase Storage and a list of controls for the next release.
What does “file upload accepts any file type” mean?
It means that your server stores a file without a check of what the file is. A user can upload a script, a web page, an executable, or a very large file instead of the image that you expect.
The risk depends on what happens to the file afterward. Three cases are common:
| What your app does with the file | What an attacker can do |
|---|---|
| Serves it from your own domain | Upload an HTML or SVG file that runs scripts in the browser of a visitor |
| Saves it in a folder that the server runs as code | Upload a script and run it on your server |
| Stores it without a size limit | Fill your storage and raise your bill |
| Passes it to an image or document parser | Trigger a bug in the parser |
AI coding tools create this problem often. You ask for an avatar upload. The tool adds <input type="file" accept="image/*"> and a route that saves the file. The upload works for a normal user, so the missing server check stays hidden.
The browser attribute gives a false sense of safety. The MDN page on the accept attribute says that it “doesn’t validate the types of the selected files” and that you should validate the requirement on the server. A user can often switch the file picker to “All Files” and select anything. A script can send the request with no picker at all.
How do I confirm that my upload accepts any file?
Test your own app only. Use a test account.
- Find the upload route. Open the Network tab in the browser DevTools. Upload a normal image. Note the URL, the method, and the field name of the request.
- Create a test file. Make a text file named
test.htmlthat contains the textupload test. Do not use real malicious content. - Upload the test file through the form. Change the file picker to “All Files” if the picker hides it. If the app accepts it, the browser check is the only control.
- Upload the test file directly. Use
curland send the file to the route. This test skips the browser. - Change the name and the type. Send the same text with the name
photo.png. Then send an image with the namephoto.html. Observe what the server accepts. - Open the returned URL. If the server stores the file and serves it, see how the server serves it. Check the
Content-Typeheader of the response.
curl -i -X POST https://your-app.example/api/upload \
-H "Authorization: Bearer YOUR_TEST_TOKEN" \
-F "file=@test.html;type=image/png"
Replace the URL, the header, and the field name with the values from your own app. The type=image/png part tells the server that the file is an image, while the content is text. If the response is a success, the server trusts the header that the sender wrote.
Warning: Do not upload real malware, and do not test routes that belong to other people. Use a harmless text file.
Also read your code. Search for the upload handler. Look for a check of the file on the server. If you see only the form attribute, or only a check of the Content-Type header, the route is open.
What do I do first? Fix it now, in order of urgency
Do the steps in this order. Steps 1 and 2 reduce the risk of the files that already exist.
Caution: If you delete files, keep a copy for the investigation. You may need the files to see what an attacker did.
- Stop serving uploads from your main domain. Serve user files from a separate domain or from a storage service. A script in an uploaded file then cannot read the cookies of your main site.
- Serve uploads as downloads. Set the
Content-Disposition: attachmentheader for file types that a browser could treat as a page. Set theContent-Typeheader yourself. AddX-Content-Type-Options: nosniff, which, according to MDN, makes the browser use the declared type without a guess from the content. - Add a server-side allow-list. Accept only the extensions and types that your feature needs. See the next section.
- Set a size limit. Reject files above the limit before you read the whole body.
- Rename each file. Never use the name from the user as the stored name.
- Move uploads out of the code path. The upload folder must not be a place where the server runs files as code.
- Review the files that exist. Read the damage section.
- Deploy and repeat the tests. Send the test files again.
How do I validate a file upload on the server?
Use several checks. The OWASP File Upload Cheat Sheet gives the main rules. Each check catches a different mistake.
| Check | What it catches | Limit |
|---|---|---|
| Extension allow-list | Files with an extension that you do not accept | A sender can change the name |
Content-Type header |
Accidental mistakes | The sender writes the value |
| File signature | Files with content that does not match the type | Easy to bypass when used alone |
| Size limit | Very large files | Needs a limit in the proxy and the storage service |
| New file name | Name tricks such as ../ and double extensions |
None |
The OWASP page says that you must list the allowed extensions and accept only the safe ones that your feature needs. It says that you must not trust the Content-Type header, because the sender can spoof it. It also says that a signature check must not be the only check, because bypass is common and easy.
The page lists .jpg.php as an example of a double extension that defeats a simple filter. Validate the extension of the final name after the server decodes it, and use the allow-list on that value.
This example for Node checks a buffer. It uses an allow-list for the extension, a size limit, and the first bytes of the file:
const allowed = {
".png": [0x89, 0x50, 0x4e, 0x47],
".jpg": [0xff, 0xd8, 0xff],
".pdf": [0x25, 0x50, 0x44, 0x46],
};
const maxBytes = 2 * 1024 * 1024;
function checkUpload(originalName, buffer) {
const ext = originalName.slice(originalName.lastIndexOf(".")).toLowerCase();
const signature = allowed[ext];
if (!signature) return "type not allowed";
if (buffer.length > maxBytes) return "file too large";
const matches = signature.every((byte, i) => buffer[i] === byte);
if (!matches) return "content does not match type";
return null;
}
The function returns an error text or null. Your route returns 400 for an error. The first bytes are the signature, also called the magic bytes, of each format. The check is not complete, but it blocks the simple bypasses.
Then store the file under a new name. The OWASP page recommends that you change the file name to a name that the application generates, such as a random string.
import { randomUUID } from "node:crypto";
const storedName = `${randomUUID()}${ext}`;
Never build a path from the original name. Use the generated name and a fixed folder.
Set the size limit in more than one place. Your web framework, your proxy, and your storage service each need a limit. The server-side limit stops large bodies early. The OWASP page also notes that limits must consider the size of a file after decompression, for example for ZIP files.
If you accept archives or XML-based documents, read file upload, zip slip, and XXE. Those formats have extra risks. The simplest fix is not to accept them.
How do I restrict file types in Supabase Storage?
Supabase buckets can enforce a type list and a size limit. The Supabase bucket documentation shows the options allowedMimeTypes and fileSizeLimit when you create a bucket:
const { data, error } = await supabase.storage.createBucket("avatars", {
public: false,
allowedMimeTypes: ["image/png", "image/jpeg"],
fileSizeLimit: "1MB",
});
The example in the documentation uses image/* and a public bucket. This version uses a narrow list and a private bucket. Allow only the types that you need.
Supabase also has a global file size limit in the Storage settings. The file limits documentation says that a limit for a single bucket cannot be higher than the global limit.
Two cautions apply:
- The bucket type list is not a content check. The storage service checks the declared type. It does not read the bytes. Keep the signature check for sensitive routes.
- Storage policies still apply. A user who can upload to a bucket needs an insert policy on
storage.objects. Restrict the policy to the folder of that user, for example by the user ID. For the full set of policies, read the Supabase guide.
If a bucket is public, any person who knows the file URL can read the file. Use a private bucket and a signed URL for files that are not meant for everyone.
How do I check for files that someone already uploaded?
Look in the upload folder or the bucket. Do the review in this order.
- List all files by extension. Look for extensions that your feature does not need:
.html,.htm,.svg,.js,.php,.exe,.sh. - List the largest files. Look for files that are much larger than a normal upload.
- List the newest files. Compare the upload times with your real user activity. Look for bursts from one account.
- Check the file contents. Open suspicious files as text in a safe editor. Do not open them in a browser on your main domain.
- Match each file to a user. If a file has no owner record, treat it as suspicious.
- Read your access logs. Find the requests that fetched the suspicious files. If visitors loaded an HTML or SVG file from your domain, a script may have run in their browsers.
- Quarantine the files. Move them to a private place. Delete them after you finish the review.
If a script file could run on your server, check your server for changes. Look for new files, new users, and new scheduled jobs. If you find any, treat the server as compromised. Rebuild it from a clean image and rotate all secrets.
If visitors could run a script from your domain, rotate the session secrets and sign users out. An attacker script can read data that the visitor can see on your site.
How do I stop it from happening again?
Use these controls for each upload feature.
- Write the allow-list in one place. Keep one list of accepted types, sizes, and folders. Each upload route uses it.
- Keep the
acceptattribute for the user. It is useful for normal users. Do not count it as a control. - Rename and store files in a private location. Use random names. Serve files by an ID, and check that the user may read the file. See BOLA in AI-generated CRUD for the owner check.
- Use a separate domain for user files. This control removes most script risks.
- Set the response headers. Use
X-Content-Type-Options: nosniff, a correctContent-Type, andContent-Disposition: attachmentfor types that you do not need to show inline. - Limit the rate of uploads per user. A limit stops storage abuse. See API abuse protection.
- Add a rule for your AI tool. Write in the project instructions: “Every upload route must check the extension, the size, and the file signature on the server, and must rename the file.”
- Scan the deployed app. A scan sends files of the wrong type to your routes and shows which routes accept them.
When should I get help?
Get help in these cases:
- You find script, executable, or web page files that you did not expect, and you cannot say if anyone loaded them.
- You think that an uploaded file ran on your server. Take the server offline and ask a security engineer to review it.
- The uploads contain personal data, for example ID documents, and the bucket was public. You may have a legal duty to report the exposure.
- Your feature must accept complex formats such as archives, office documents, or video. These formats need a parser that you must keep updated, and a review is worth the cost.
- You do not know all the places that accept uploads. A scan of the deployed app lists the routes that take files.
After the fix, repeat the tests from the confirm section. The test.html file must be rejected on every route, with a 400 status, both through the form and through curl.
Common questions
Does the HTML accept attribute stop other file types?
Can I trust the Content-Type header of an upload?
Is it enough to check the file extension?
Are SVG files safe to accept?
How large should the upload limit be?
Test your upload routes
A VibeEval scan sends files of the wrong type to the upload routes of your deployed app and reports the routes that accept them.
14-day free trial · No credit card · Cancel anytime