Common security risks in file upload and sharing platforms stem from unrestricted file intake and uncontrolled delivery. Intake risks include malware injection, active content execution (e.g., malicious scripts in SVG/HTML), and denial-of-service via resource exhaustion. Sharing risks include broken access control, permanent or guessable links, and data leakage via unstripped EXIF metadata.
A user uploads a file, and a week later someone else opens it from a link. Every step between those two moments is a place where the feature can be abused.
The common security risks in user file upload and sharing platforms fall into two groups. Upload risks are malicious files (malware, scripts hidden in SVG or HTML, files that claim to be one type and are another) and uploads large or numerous enough to exhaust your servers. Sharing risks are share links that never expire or can be guessed, missing per-file access checks, files served from your own domain, and metadata such as GPS coordinates that leaves with the file. Malicious and oversized files are stopped at intake. Access checks and link lifetime are set at delivery.
Filestack, our API for uploading, processing and delivering files, has a feature at each of those points. Security policies put a signed, expiring permission on every upload and every link. Virus Detection scans each file and stores a copy only when it’s clean. Our CDN serves files from its own domain, and image transformations make copies without location data. Together they make up our secure file upload layer, and for this post we wrote a short Node script that signs the upload policy and the share link.
Key Takeaways
- Upload risks come from what a user sends you. Sharing risks come from who can open the file afterward and for how long.
- A file’s name and its declared type are both set by the client, so decide what a file is by reading its contents.
- A share link should open one file, for reading only, for as long as the task needs.
- With application security on, every upload and download needs a signed policy, and the policy’s expiry ends a share link without a revoke step.
- Photos carry metadata that travels with the original file. Share a processed copy when the location shouldn’t leave with it.
What are the main security risks of file uploads?
The main security risks of file uploads are files that carry malware, files that run code when opened, files whose type is not what they claim, and uploads that use up storage, memory or processing time. Together they make up OWASP unrestricted file upload, the class of bug where an application accepts a file without checking what it is or where it ends up.
| Risk | What happens | Where it’s stopped |
|---|---|---|
| Malware | An infected file is stored and later downloaded by other users | Intake, with a virus scan |
| Active content | An SVG or HTML file carries a script that runs when someone opens it | Intake and delivery |
| Fake file type | A file named photo.jpg holds something else | Intake, by reading the contents |
| Resource exhaustion | Very large files, or thousands of small ones, fill storage or stall processing | Intake, with size and count limits |
| Path and storage abuse | A crafted filename or path writes outside the intended folder | Storage |
Malware is stopped by scanning every file before other users can download it. A virus detection API scans each file after it lands and reports whether it carries a known virus or other malware.
Active content hides in formats that look harmless, leading to vulnerabilities like CWE-79 (Cross-Site Scripting). An SVG is an image and also an XML document, and it can carry <script> tags. An upload form that accepts SVG needs the same care as one that accepts HTML, and our walkthrough of script injection in SVG files shows how the attack runs and how to stop it.
Resource exhaustion isn’t only about big files. Thousands of tiny uploads can cost more than one large one, because each file brings its own request, scan and database row. Limits on file count and a processing queue keep small files from becoming a DoS risk.
Which risks come from sharing files rather than uploading them?
Sharing adds risks that have nothing to do with the file’s contents. They come from who can open the file, for how long, and what else the file reveals.
- Permanent links. A link that never expires keeps working after a project ends, after an employee leaves and after the link is pasted into a public channel.
- Guessable links. A URL built from a sequential ID, such as
/files/1042, lets anyone try/files/1043. - Missing per-file access checks. The app checks that a user is logged in, then serves any file the user asks for. This is broken access control, the first entry in the OWASP Top 10.
- Metadata leaks. A phone photo usually carries EXIF metadata, often with the GPS coordinates of where it was taken. Sharing the original file shares that location.
- Files served from your own domain. A shared HTML or SVG file that opens on your app’s domain can run its scripts with access to your app’s cookies.
Files delivered from cdn.filestackcontent.com open on our CDN’s domain, not yours, so a script inside a shared file can’t read your app’s cookies or local storage. Direct file URLs also carry a Content-Security-Policy header that blocks inline scripts. If you put a custom domain in front of the CDN, pick one that your session cookies aren’t scoped to.
Metadata needs a processed copy. On a JPEG, the compress transformation removes the EXIF metadata, GPS coordinates included, unless you pass compress=metadata:true. resize and output=format:jpg keep the metadata, so a resized copy still carries the location.
How do you threat model an upload and sharing feature?
A threat model for an upload and sharing feature follows one file through four stages and asks the same three questions at each one: who can reach this stage, what do they control, and what goes wrong if they abuse it.
| Stage | Who controls what | What can go wrong |
|---|---|---|
| Intake | The uploader controls the bytes, the filename, the declared type, the size and how many files arrive | Malware, active content, fake types, resource exhaustion |
| Processing | Your code and its libraries parse the file | A malformed file crashes or exploits an image or document parser |
| Storage | Your app picks the path, bucket and permissions | Path traversal, public buckets, files kept longer than needed |
| Delivery | Anyone holding the URL | Permanent or guessable links, missing access checks, metadata leaks, scripts running on your domain |
On a platform built on user uploads, such as a marketplace, a community or a document portal, the person who uploads a file and the people who open it are usually different, so one bad upload reaches every viewer. A user-generated content pipeline puts scanning and moderation between the two.
The model also tells you which checks can run in the browser. None of the intake checks can rely on it alone. The browser is where the attacker sits, so a file type filter in the picker is a convenience for honest users, and the real limit lives in a signed policy your backend issues.
There are three common ways to put these controls in place. You can build them on your own servers, assemble them from your cloud storage provider’s building blocks, or put an upload API in front of storage.
| Option | How it covers the risks | The catch |
|---|---|---|
| Your own servers | ClamAV for scanning, a library such as libmagic for type checks, signed URLs from your web framework, and your own EXIF stripping | You run and update the scanner, its signature database and every file parser, next to your app |
| Cloud storage building blocks | Presigned URLs from Amazon S3 or Google Cloud Storage, bucket policies, and a scanning service triggered by storage events | Each control is a separate piece to wire together, and an S3 presigned URL signed with Signature Version 4 lasts at most seven days |
| Filestack | Security policies for uploads and links, Virus Detection in a workflow, delivery from our CDN’s domain, and transformations for clean copies | Your app still decides which user may open which file |
We built Filestack for teams that would rather not run a scanner and a fleet of file parsers beside their app. The files are scanned and processed on our side of the upload, and your code signs policies and reads results.
How does Filestack handle each of these risks?
Four Filestack features cover file upload and sharing risks: security policies, Virus Detection, delivery from our CDN, and image transformations. Each one maps to a risk from the tables above:
| Risk | Filestack feature | Where it runs |
|---|---|---|
| Malware | Virus Detection in a workflow, with a store task that keeps only clean files | Intake |
| Fake file type | Virus Detection reads the contents, whatever the name or declared type | Intake |
| Oversized uploads | maxSize in the upload policy |
Intake |
| Unauthorized uploads | An upload policy limited to the pick and store calls, with a short expiry |
Intake |
| Permanent or guessable links | A signed read policy for one handle, with an expiry |
Delivery |
| Scripts on your domain | Files served from cdn.filestackcontent.com |
Delivery |
| Metadata leaks | A compress copy of the photo |
Delivery |
A file’s handle is a 20-character ID such as kxqdtjH1RFyA1rNZyZZs, not a sequential number, so one file’s URL doesn’t lead to the next. The per-file access rule is the one row that stays in your app, because only your app knows which user owns which file.
Which risks are stopped at intake, and which at delivery?
Malware, active content, fake types and oversized uploads are stopped at intake, before the file reaches other users. Link lifetime, per-file access and metadata are handled at delivery, every time the file is opened.
Intake controls
Start with what the upload is allowed to do. A security policy is a small JSON document your backend signs with your app secret. It lists the calls a request may make and when it expires, and for uploads it can also set a maxSize in bytes. We wrote share-link.mjs, a plain Node script with no packages, to sign both kinds of policy this post uses. It needs Node 20.6 or later for --env-file, and a .env file holding your app secret from the Developer Portal:
FILESTACK_APP_SECRET=YOUR_APP_SECRET
The upload half of the script signs a policy that allows uploading and nothing else:
import { createHmac } from 'node:crypto';
const CDN = 'https://cdn.filestackcontent.com';
function sign(policy) {
const encoded = Buffer.from(JSON.stringify(policy))
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_');
const signature = createHmac('sha256', process.env.FILESTACK_APP_SECRET)
.update(encoded)
.digest('hex');
return { policy: encoded, signature };
}
const inMinutes = (minutes) => Math.floor(Date.now() / 1000) + minutes * 60;
// Upload only, files up to 10 MB, valid for 5 minutes. Your backend hands this to the uploader.
export const uploadPolicy = () =>
sign({ call: ['pick', 'store'], maxSize: 10 * 1024 * 1024, expiry: inMinutes(5) });
Return uploadPolicy() from an endpoint on your backend, so the app secret never reaches the browser. A policy with pick and store can’t read, overwrite or delete anything already stored.
Type checks come next. The filename and the Content-Type of an upload are both set by the client, so neither proves what the file is. A virus scan reads the contents instead. Our scan flags the EICAR test file whether it is named eicar.com, packed inside a ZIP, packed inside a ZIP inside a ZIP, or renamed holiday.jpg and stored as image/jpeg.
The scan runs as a task in a workflow, and the workflow can store a clean copy only when the scan passes. Virus Detection runs on Start, Grow and Scale. In the Developer Portal, open your application, then Workflows, and build a workflow with two tasks:
- A
virus_detectiontask namedvirus_scan. - A
storetask namedstore_clean, with the condition pathinfected, conditioneqand valuefalse.
Attach the workflow by passing its ID in the upload options, workflows: [WORKFLOW_ID] in filestack-js client.upload or storeTo.workflows in the picker. The workflow runs under the upload’s policy, so add read, convert and runWorkflow to the call list in uploadPolicy. For a clean file, the finished job carries both results, with the handle of the stored copy:
{
"results": {
"store_clean": {
"data": {
"filename": "clean.txt",
"handle": "FTKGmO1R5qYCfuA4jMr2",
"type": "text/plain",
"url": "https://cdn.filestackcontent.com/FTKGmO1R5qYCfuA4jMr2"
}
},
"virus_scan": { "data": { "infected": false, "infections_list": [] } }
},
"status": "Finished"
}
For an infected file, results holds virus_scan with "infected": true and the detection name, and no store_clean, because the condition didn’t match. Serve the store_clean handle, never the original, and other users only ever download files that passed the scan. The virus scanning workflow post covers reading the job, deleting infected originals and receiving the result on a webhook.
Delivery controls
Delivery controls run every time a file is opened. With application security turned on in the Developer Portal, every request, uploads and downloads included, needs a signed policy. That turns each file URL into something your backend issues on purpose, instead of a permanent public address.
A share link should stay valid for as long as the task behind it takes, and no longer. A download button needs minutes. A document sent for review needs hours or a few days. A file that must stay reachable for months is better served behind your own login, with a fresh short link created on each visit.
The second half of share-link.mjs makes that link. It signs a policy for one file, allows only read, and sets the expiry in minutes:
// Opens one file, read only, until the link expires
export function shareLink(handle, minutes) {
const { policy, signature } = sign({ call: ['read'], handle, expiry: inMinutes(minutes) });
return `${CDN}/security=p:${policy},s:${signature}/${handle}`;
}
const [handle, minutes = '15'] = process.argv.slice(2);
if (handle) console.log(shareLink(handle, Number(minutes)));
Run it with a file’s handle and the number of minutes the link should last:
node --env-file=.env share-link.mjs HANDLE 15
The script prints the link, with the policy and the signature in the path:
https://cdn.filestackcontent.com/security=p:eyJjYWxsIjpbInJlYWQiXSwi...,s:7c51d642.../HANDLE
The policy inside decodes to {"call":["read"],"handle":"HANDLE","expiry":1792396632}. A valid policy needs an expiry in the future, so once that time passes the link stops working, and nobody has to revoke it. Opening the link before then returns the file. This command keeps three of the response headers:
curl -sI "PASTE_THE_LINK_HERE" | grep -i -E "^HTTP|content-type|content-security"
HTTP/2 200
content-type: text/plain; charset=utf-8
content-security-policy: default-src 'self'; font-src 'self'; img-src 'self'; script-src 'self'; style-src 'unsafe-inline';
Your app still decides who gets a link. Check that the signed-in user may open this file, then call shareLink, and keep the minutes short enough that a forwarded link expires before it travels far. The signed link expiry demo shows a link going from working to expired in the browser.
Questions people ask
What is a file upload vulnerability?
A file upload vulnerability is any way an attacker can use an upload feature to store, run or spread something the application didn’t intend. The most common form is CWE-434 unrestricted upload, where a server accepts a dangerous file type, such as a script, and then serves or runs it.
Can uploaded files be scanned for viruses automatically?
Uploaded files can be scanned automatically by attaching a scan to every upload. With Filestack, a virus_detection task in a workflow scans each file after it is stored and returns an infected value and the names of any detections. A conditional store task then keeps a copy only of the clean files.
Is file sharing riskier than file uploading?
File sharing and file uploading carry different risks rather than more or fewer. Upload risks depend on what the file contains. Sharing risks depend on who can open the file and for how long. A clean file shared through a permanent public link can still leak data, and a malicious file is a risk even if nobody shares it.
Does checking the file extension stop malicious uploads?
Checking the file extension doesn’t stop malicious uploads, because the uploader picks the name. A file named holiday.jpg can hold anything. Checks that read the contents, such as a virus scan or decoding the image, show what the file is.
What is content disarm and reconstruction?
Content disarm and reconstruction (CDR) rebuilds a file from the parts known to be safe and drops everything else, such as macros, embedded scripts and attachments. Antivirus scanning looks for known malware. CDR removes active content whether or not it is known, so the two are often used together on document uploads.
Signed URLs stop anyone without the link from opening the file, and they stop the link from working after it expires. They don’t check who is holding the link. That check belongs in your app, before the link is created, and a short expiry limits how far a forwarded link can travel.
What to build next
Two pieces stay with your app. The first is the access rule that decides which user may open which file, and it runs before shareLink. The second covers the storage stage of the threat model, deleting files once nobody needs them. A delete request takes a policy with the remove call for that one handle, signed with the same sign function.
To run the share link script against your own files, create a free account for an API key, then copy the app secret from the Security section of your application in the Developer Portal.
Joshua is a web developer with over 4 years of experience building responsive, high-performance websites and web applications. Currently working as an AI Automation Specialist, he combines modern web development with automation to create efficient, scalable digital solutions. He shares practical insights on WordPress, web development, and emerging technologies.
Read More →


