Secure File Upload Forms: How to Accept Files Safely
Build a secure file upload form: validate on the server, cap upload size limits, keep files private behind signed links, and stop malicious file uploads.
6 min read

A secure file upload form treats every uploaded file as untrusted: it validates type and size on the server, stores files privately, serves them through short-lived signed links, and deletes them when they are no longer needed. The browser-side checks you add are for usability, not protection.
If you need to accept files on your website, such as resumes, photos, invoices or design briefs, the upload field is the most dangerous input on the page. A text field can carry spam. A file field can carry malware, a disguised script, or someone else's personal data that you are now responsible for. This guide covers the main risks and the defenses that actually work.
Why file upload security matters
A form that accepts files is effectively a public write endpoint into your storage. Here is what can go wrong:
- Malicious file uploads. Executables, macro-laden documents or files carrying exploits that target whoever opens them, usually you or your client.
- Content-type spoofing. A file named
invoice.pdfwith aContent-Type: application/pdfheader can contain anything. Both the extension and the MIME type are set by the client. - Server-side execution. On classic hosting, an uploaded
.phpor.htmlfile saved into a web-accessible folder can be executed or rendered by the server, turning an upload into a site takeover or a phishing page on your domain. - Public URLs. Files stored at predictable public paths can be found, shared or indexed. If those files are CVs or ID scans, that is a data leak.
- Storage abuse. Bots and bad actors can fill your disk or cloud bucket, raising costs or taking the form down.
- Personal data. Uploaded documents often contain names, addresses and phone numbers. Keeping them forever creates risk with no benefit.
Defenses for a secure file upload form
1. Treat the accept attribute as UX only
The accept attribute narrows the file picker so honest users pick the right file type. It does not stop anyone. A script can post any file to your endpoint without ever loading your page.
<input type="file" name="resume" accept=".pdf,.doc,.docx">Keep it, because it helps real visitors, but never rely on it.
2. Validate on the server
Every check that matters has to happen where the attacker cannot edit the code:
- Check the file extension against an allowlist, not a blocklist.
- Inspect the file's actual bytes (its "magic number" signature) rather than trusting the declared MIME type.
- Reject double extensions such as
photo.jpg.php. - Generate your own storage filename instead of reusing the one the user sent. Keep the original name only as metadata.
- Limit the number of files per submission.
3. Enforce an upload size limit
Size limits protect your storage, your bandwidth and your inbox. Enforce them per file and per request, on the server. Platforms and hosting providers usually impose their own request size limits too, so a very large file may be rejected before your code even sees it. Pick a limit that covers legitimate use and nothing more.
4. Store files privately and use signed URLs
Uploaded files should never sit in a public folder. Store them in private object storage or outside your web root, and hand out access through:
- An authenticated dashboard for the site owner, and
- Signed URLs that expire, for links you send by email or to other tools.
Signed links mean a forwarded email or a leaked log line does not give permanent access to the file.
5. Prevent inline execution
When serving a file back, send headers that stop the browser from rendering it as part of your site:
Content-Disposition: attachmentso files download rather than open inline.X-Content-Type-Options: nosniffso the browser does not guess a more dangerous type.- Serve uploads from a separate domain or storage host, never from your main application origin.
6. Scan when the risk justifies it
If files will be opened by many people or processed automatically, consider running them through a malware scanner before anyone downloads them. At minimum, open unknown documents with care, keep office software patched, and be suspicious of macro prompts.
7. Set a retention period
Delete files when their purpose is done. A job application form does not need to keep resumes for years. Short retention reduces both storage costs and the damage from any future breach. See our post on GDPR contact forms for more on data minimization.
A correct HTML file upload form
File inputs only work in a plain HTML form when the form uses POST and multipart/form-data. Without the enctype, the browser sends only the filename.
<form action="https://formsubmit.app/f/YOUR_FORM_ID"
method="POST"
enctype="multipart/form-data">
<label for="name">Name</label>
<input id="name" type="text" name="name" required>
<label for="email">Email</label>
<input id="email" type="email" name="email" required>
<label for="resume">Resume (PDF or Word)</label>
<input id="resume" type="file" name="resume" accept=".pdf,.doc,.docx" required>
<input type="text" name="_gotcha" tabindex="-1" autocomplete="off"
style="display:none" aria-hidden="true">
<button type="submit">Send application</button>
</form>The hidden _gotcha field is a honeypot that catches simple bots before they can upload anything. For more layers, see how to stop form spam.
Handling large files with direct uploads
Sending a big file through a normal form post means the whole request passes through your server or serverless function, and many hosts cap request bodies at a few megabytes. The common solution is direct uploads:
- The browser asks your backend for permission to upload a specific file.
- The backend checks the request (size, type, limits) and returns a short-lived upload token.
- The browser uploads the file straight to object storage using that token.
- The form then submits normally, sending a reference to the stored file instead of the file itself.
This keeps large payloads off your application server while the backend still decides what is allowed. It does require JavaScript, so it is usually reserved for files above the plain-form request limit.
File upload security checklist
| Check | Why it matters |
|---|---|
method="POST" and enctype="multipart/form-data" | Files are not sent otherwise |
accept attribute on the input | Better UX, not security |
| Server-side type allowlist and content inspection | Stops spoofed extensions and MIME types |
| Per-file and per-request size limits | Prevents storage and bandwidth abuse |
| Random storage filenames | Avoids path tricks and overwrites |
| Private storage, no public folder | Prevents leaks and indexing |
| Signed, expiring download links | Limits damage from forwarded links |
Content-Disposition: attachment and nosniff | Stops inline rendering and execution |
| Spam protection before upload | Keeps bots from filling storage |
| Retention policy | Minimizes stored personal data |
Using a form backend instead of building it
You can build all of this yourself, but it means writing validation, configuring storage, generating signed URLs and running cleanup jobs. A form backend handles the storage side for you. With FormSubmit, file uploads are stored privately, deleting a submission deletes its files, and per-form retention can purge old submissions automatically. You still choose what to accept and who sees it.
If you want a head start, the file upload form template generates the HTML above with your fields, and the pricing page lists the file size and storage limits for each plan.
Last updated .


