File upload security: what to build, what to buy and what actually stops malware


Published · Ihor Havrysh


File upload security - what to build, what to buy and what actually stops malware

The short answer: a secure upload path needs four things - validation that doesn't trust the filename, storage that can't execute, a serving route that can't be turned against you and a deliberate decision about malware. That last one is where guides stop. Scanning tells you a file is known to be bad. Sanitising makes it safe without needing to know. Work out which one your application needs.

The essentials:

  • Validation is necessary, and it has a ceiling.
  • Scanning and sanitising answer different questions.
  • Your cloud platform may already do the scanning part.
  • Plenty of applications need none of this.

Written for developers, technical founders and product owners who accept files from users and have been asked to make that safe - by a pen test, a security review, a client questionnaire or their own judgment.

Accepting file uploads is one of those features that looks finished long before it's safe. The form works, the file arrives, the customer is happy. The security work is invisible until it isn't.

There's no shortage of advice on this. OWASP publishes a thorough cheat sheet, PortSwigger teaches the exploitation mechanics properly and every security vendor has a ten-point list.

What almost none of them tell you is what to build, what it costs or how to choose - and they nearly all treat "scan the file for malware" as a single step, when it's really a fork in the road.

This guide covers the full surface: what goes wrong, the controls in build order, what each stops and where it can be got around. Then the part that's usually missing - the four ways to handle malware, what they cost and how to work out which one you need.

#12 where unrestricted file upload ranks in MITRE's 2025 CWE Top 25, scored on published CVEs rather than breach counts
4 file-upload flaws CISA confirmed as exploited in the wild in the same period - a floor, not a total
31% of breaches began with vulnerability exploitation, now ahead of credential abuse (Verizon DBIR 2026, 22,000+ breaches)
56-99% spread of offline detection rates across 20 engines against 10,030 already-circulating samples (AV-Comparatives, March 2026)

What actually goes wrong with file uploads?

Uploads go wrong in more ways than "someone sends a virus". It's worth separating them, because the controls are different and some of the nastiest failures have nothing to do with malware at all.

Five ways an upload path fails: code execution, path traversal, attacks on users, reconfiguration and resource exhaustion
Only the first of these is about malware. The other four are about where the file lands and what happens to it next.

Code execution on your server

The classic. An attacker uploads something your server will execute - a PHP script, an ASP page, a shell - then requests it. MITRE catalogues this as CWE-434, Unrestricted Upload of File with Dangerous Type, at number 12 in the 2025 CWE Top 25.

That rank is worth reading carefully. MITRE scores weakness types by how often they appear in published CVEs and how severe those are, so it measures flaws in software products rather than how often organisations get breached. The persistence is the interesting part, not the position: CWE-434 was first catalogued in 2006 and is still in the top half of the list 20 years later.

Path traversal

Not what's in the file, but where it lands. If the filename builds a path without normalisation, ../../ sequences let an attacker write outside the upload directory - over a config file, into a cron directory, into the web root.

In 2026 that was the flaw in Langflow (CVE-2026-5027): the upload endpoint took the filename verbatim from the Content-Disposition header and passed it to storage unmodified. CISA added it to the Known Exploited Vulnerabilities catalogue.

Attacks on your other users

If uploaded files are served back to people, the target may not be your server at all. An SVG is XML and can contain script, so an image allow-list that includes SVG is also a script allow-list.

An HTML file served from your domain runs in your origin. Even a file that's never rendered can be a delivery mechanism - your application becomes the trusted place the malware came from.

Changing how other files are served

A subtler one, usually missing from the checklists. Some uploads don't attack directly - they reconfigure. A dropped .htaccess can tell Apache to execute image files as PHP, turning your carefully allow-listed .jpg into a live script.

When the Ninja Forms File Upload flaw was mass-exploited in April 2026, malicious .htaccess files were among the observed payloads, alongside web shells and image-headed PHP files. The control here isn't file type - it's making sure the upload directory can't change its own rules.

Resource exhaustion

Large files, many files or small files that become large - a deeply nested archive can unpack to thousands of times its compressed size. None of this needs malware. It just needs limits you didn't set.

These are design failures, not coding slips. OWASP's 2025 Top Ten puts unrestricted upload (CWE-434), reliance on the filename or extension (CWE-646) and upload race conditions (CWE-362) in one category: A06:2025 Insecure Design - explicitly for weaknesses that can't be fixed by writing more careful code, because the problem is in the shape of the thing. That's the lens for everything below. You're designing a path files travel along, not adding checks to a form handler.

The controls, in build order

Roughly the order you'd implement them. For each one: what it stops and how it's got around - because a control you trust more than it deserves is worse than one you know the limits of.

The eight file upload controls in build order, from allow-listing types to setting a retention limit
All eight are code you write once. None of them decides what you do about malware - that comes later, and separately.

1. Allow-list the types you need

List what you accept, not what you reject. Deny-lists lose by default: they have to anticipate every dangerous extension, on every server configuration, forever. Allow-lists fail closed.

How it's bypassed: by attacking the parsing rather than the list. Double extensions (shell.php.jpg), trailing dots or whitespace, null bytes and URL or double-URL encoding of the separator. OWASP documents the family. The bypass is nearly always that the check and the filesystem disagree about what the filename is.

This is not a museum piece. In July 2026 CISA's weekly bulletin carried CVE-2026-61457 in the Grav CMS API plugin: its validateFileExtension() inspected only the final extension via pathinfo(), so shell.php.jpg walked straight past the blocklist and the web server executed it as PHP. Textbook bypass, textbook deny-list, shipped in production in 2026.

2. Don't trust the Content-Type header

It's supplied by the client. It's a hint about intent, not a fact about content, and changing it takes seconds. Use it for a fast rejection of obvious mistakes, never as a security control. OWASP is unambiguous on this, and MITRE has a dedicated weakness for it - CWE-646, reliance on the file name or extension of an externally-supplied file.

3. Verify the content, not the label

Read the first bytes and check them against what the extension claims. This is a real control and the step most often skipped.

Signature check - C#
static readonly Dictionary<string, byte[][]> Signatures = new()
{
    ["png"]  = [[0x89, 0x50, 0x4E, 0x47]],
    ["jpg"]  = [[0xFF, 0xD8, 0xFF]],
    ["pdf"]  = [[0x25, 0x50, 0x44, 0x46]],
    ["docx"] = [[0x50, 0x4B, 0x03, 0x04]]   // any zip container
};

static bool SignatureMatches(Stream file, string declaredExtension)
{
    if (!Signatures.TryGetValue(declaredExtension, out var candidates))
        return false;                       // unknown type - reject

    var header = new byte[8];
    file.ReadExactly(header, 0, header.Length);
    file.Position = 0;

    return candidates.Any(sig => header.Take(sig.Length).SequenceEqual(sig));
}
Reject unknown types rather than letting them through - an allow-list that fails open isn't one.
Signature check - Python
SIGNATURES = {
    "png":  [b"\x89PNG"],
    "jpg":  [b"\xff\xd8\xff"],
    "pdf":  [b"%PDF"],
    "docx": [b"PK\x03\x04"],   # any zip container
}

def signature_matches(fileobj, declared_extension: str) -> bool:
    candidates = SIGNATURES.get(declared_extension)
    if not candidates:
        return False            # unknown type - reject

    header = fileobj.read(8)
    fileobj.seek(0)
    return any(header.startswith(sig) for sig in candidates)
Seek back to the start afterwards - whatever handles the file next expects to read it from the beginning.
Signature check - Node
const SIGNATURES = {
  png:  [Buffer.from([0x89, 0x50, 0x4e, 0x47])],
  jpg:  [Buffer.from([0xff, 0xd8, 0xff])],
  pdf:  [Buffer.from('%PDF')],
  docx: [Buffer.from([0x50, 0x4b, 0x03, 0x04])]   // any zip container
};

function signatureMatches(header, declaredExtension) {
  const candidates = SIGNATURES[declaredExtension];
  if (!candidates) return false;                  // unknown type - reject

  return candidates.some(sig => header.subarray(0, sig.length).equals(sig));
}
Same shape in all three languages, because the control is a decision rather than a library call.

Two things that table won't tell you. A .docx, .xlsx and .zip share a signature, because Office files are zip containers - a signature check confirms the container, not the contents. And this control has a ceiling, which is important enough to have its own section below.

4. Generate the stored filename yourself

Don't reuse what the client sent. Generate an identifier, store the original name as data if you need to display it and never let user input reach a path. This removes path traversal as a category rather than filtering for it - which is the difference between Langflow's flaw and code that can't have it.

5. Store where nothing executes

Object storage is the straightforward answer, because it doesn't execute anything by design. If files must sit on a filesystem, put them outside the document root and serve them through application code.

And make sure the directory can't reconfigure itself. An allow-list is worth much less if the same folder will honour a dropped .htaccess or web.config.

6. Control how files are served back

Set the content type explicitly rather than letting it be inferred. Use Content-Disposition: attachment where a file should download rather than render. Serve user content from a separate origin so anything that does execute lands outside your application's cookies and storage. Add X-Content-Type-Options: nosniff.

7. Set limits, in more than one place

A maximum size, a maximum count and a rate limit per user and per address. Enforce the size at the edge, at the framework and at the point of processing - a limit checked only after the file is fully buffered has already cost you the resource it was supposed to protect. Cap archive expansion explicitly if you unpack anything.

8. Decide how long you keep things

A retention limit is a security control. Files you no longer hold can't leak, and an upload directory that only ever grows is a liability that compounds quietly.

Scanning vs sanitising: the distinction almost nobody draws

Every checklist in this field has a step that says "scan for malware". Almost none of them explain that there are two fundamentally different ways to do it, and that the difference decides what you're protected against.

Scanning asks whether a file is known to be bad; sanitising rebuilds the file from valid content only
Two techniques, two different questions. Neither is a better version of the other.

Start with the ceiling on everything in the previous section. A file can satisfy every validation control you write and still be malicious. A polyglot is a file that's valid in two formats at once - and the simplest ones are trivial to make. As Searchlight Cyber put it in their analysis of a 2026 vulnerability:

"Despite sounding fancy, PHP polyglots are incredibly easy to generate. The simplest ones can be created by just sticking GIF89a right before the PHP payload."

PortSwigger documents the same idea with images, using ExifTool to hide code in JPEG metadata while leaving a perfectly valid image. Your signature check passes. Your extension matches. The file is what it claims to be, and also something else.

What this looked like in March 2026

PolyShell is the clearest recent illustration. Discovered by the Dutch e-commerce forensics firm Sansec and disclosed on 17th March 2026, it affected every production version of Magento Open Source and Adobe Commerce up to 2.4.9-alpha2 - code that had shipped in the very first Magento 2 release in 2015 and sat there for a decade.

The mechanism is worth reading as a checklist of everything above. The REST API accepted a file as part of a cart item's custom options, from an unauthenticated shopper. Validation ran the uploaded data through an image-parsing function - so a polyglot passed. Nothing reconciled the extension against the content. And the file was written to a web-accessible directory.

Sansec's own telemetry tracks what followed. No exploitation at disclosure on 17 March. First attacks in the wild on 19th March. By 24th March, more than half of all stores they could see had malicious PHP uploaded to them; by mid-April, 82%. Adobe shipped a production fix on 12th May, nearly two months after disclosure, with no backport to older supported lines.

A note on that figure: 82% is Sansec's measurement across the stores they scan, not a census of every Magento site on the internet. It's an indication of how comprehensively an easy, unauthenticated upload flaw gets exploited once the method circulates - which is the point worth taking, rather than the precise number.

Why a scanner wouldn't have helped, and what would

A signature scanner works by comparing a file against things known to be malicious. That's genuinely effective against malware in circulation. AV-Comparatives' March 2026 test put 20 engines against 10,030 recent samples: with cloud lookups enabled, protection rates ran from 99.09% to 99.98%. Signature scanning is good at its job.

Its limit is structural rather than a quality problem. A scanner can only tell you about things that have been seen before. A web shell generated for one campaign, an hour old, has nothing to match against.

The same test also shows how much the answer depends on which engine you picked. With cloud lookups disabled - the closest proxy for a purely local signature engine - detection ranged from 56.1% to 98.6% against samples that were already circulating. "We scan uploads" isn't a statement about safety until you say what with, and how.

Sanitisation works the other way round. Rather than judging the file, it takes it apart, keeps the content and structure a valid file of that type is allowed to contain, discards everything else and builds a fresh file from what remains.

A GIF89a polyglot goes in; a clean GIF comes out, with the appended PHP gone - not because anything recognised it, but because it was never part of a valid image.

Comparison of scanning and sanitising uploaded files
  Scanning Sanitising
The question it answers Is this known to be bad? Is what's left safe to keep?
Needs prior knowledge of the threat Yes No
Novel or targeted payloads Likely missed Removed if not valid content
Result The original file, plus a verdict A rebuilt file, which may lose macros and active content
Cost per file Low - it's a comparison Higher - it's a rebuild
Where it doesn't help Anything unseen A malicious link or QR code inside a structurally valid file - there's nothing to strip
The bottom two rows are the ones worth arguing about: sanitising costs more per file, and it can't read meaning.

That last row matters and it cuts against our own product, so it's worth being direct: sanitising a PDF that contains a phishing link produces a clean PDF that still contains a phishing link. Sanitisation removes active content. It doesn't read meaning.

One more thing about PolyShell, because it would be easy to draw the wrong conclusion. Sanitisation would have neutralised those payloads. So would storing the file somewhere it couldn't be executed. Either control alone breaks that attack chain - which is a better argument for defence in depth than for any particular product.

Working out whether sanitisation is what you need? That's exactly the decision our CDR API exists to serve - and the two-minute check below will tell you if the answer is no.

What you can actually build or buy

Four routes. The costs below are the providers' own published list prices, checked in July 2026.

Four ways to handle malware in uploads: self-hosted scanner, cloud platform, scanning API or sanitisation
The order matters: work down this list and stop at the first one that covers your exposure.

Route 1: run a scanner yourself

Usually ClamAV. It's open source, well maintained and free to license. It is not free to run, and the gap between those two things is where the surprises live.

The signature database is loaded into memory. The Arch Linux documentation puts the requirement at at least 1.6 GB of RAM resident, roughly doubling for a short period during updates because the new database loads before the old one is released. Community reports put the practical minimum nearer 3 GB.

That makes a 4 GB instance the floor - around £23 a month at Azure or AWS list pricing - and a tight one: size the container to steady-state usage and the daemon gets killed during the update. There's a configuration flag to avoid the spike, at the cost of pausing scanning while the database reloads.

Then the running costs that never appear on an invoice. The signature updater needs monitoring, because a stalled updater degrades detection silently rather than failing loudly - one analysis notes that update failures cause more real-world problems than CPU or memory limits. False positives need somewhere to go. Someone needs to care when it breaks at an inconvenient hour.

When it's the right answer: a mail gateway, an internal file server or a lower-traffic upload path in a team that already runs Linux hosts and has somewhere to put this. That's a lot of teams, and for them ClamAV is a sound, free, sensible choice. The argument above is about latency-sensitive application paths, not about ClamAV as a whole.

Route 2: use what your platform already gives you

This is the option most guides skip, for the straightforward reason that most guides are written by people selling something else.

Azure: malware scanning is an add-on to Microsoft Defender for Storage - £7.50 per storage account per month, plus about 11p per GB scanned, for Blob Storage. It's agentless with nothing to run, and you can cap the monthly scanned volume so the bill can't run away.

AWS: GuardDuty Malware Protection for S3 charges about 7p per GB scanned plus 16p per 1,000 objects evaluated, with an ongoing free tier of 1,000 requests and 1 GB a month. You don't need GuardDuty itself enabled to use it. The per-GB price dropped by 85% in February 2025.

Cloudflare has inline content scanning that inspects uploads in transit with no application change, which is a genuinely different shape - but it's Enterprise plan plus a paid add-on, so for most teams reading this it isn't on the menu. Worth knowing it exists; not worth planning around.

Run the numbers for your files before you buy anything. AWS's own worked example - 4,000 uploads totalling 350 GB - costs about £24 a month. But that's an 87 MB average file, which is video. For a document workload of 4,000 files averaging 500 KB, the same service costs about 78p a month; the Azure equivalent is around £7.70. Platform-native scanning for document-sized files is close to free, and that's worth knowing before anyone quotes you for it.

It cuts the other way too, which is why the billing unit deserves a moment. AWS charges per gigabyte and per thousand objects; Azure charges per storage account and per gigabyte; per-document APIs charge per document. Many small files punish per-object pricing. A few large files punish per-gigabyte pricing. Model your own file-size distribution rather than trusting anyone's worked example, including this one.

Route 3: call a scanning API

Several vendors sell file scanning as a hosted API with published prices, region selection and per-language SDKs. You avoid the infrastructure, you get an engine somebody else maintains and integration is usually an afternoon.

The questions worth asking are practical: which engine or engines, what happens to your file after it's scanned, whether you can pin the processing region and whether the response is a verdict you can act on or a report you have to interpret.

Route 4: sanitise instead of scanning

Send the file to something that rebuilds it and returns a clean version. You're not buying a better verdict; you're buying not needing one. It costs more per file than scanning, because rebuilding is more work than comparing, and it changes the file - macros and active content don't survive, which is the point and occasionally the problem.

This is the right route when files arrive from people you have no relationship with, when they're passed on to other users or staff or when a miss is expensive enough that "probably not known-malicious" isn't a good enough answer.

The VirusTotal trap

VirusTotal comes up in nearly every discussion of scanning uploads, and it's an excellent service. Two things about using it inside a product are worth knowing before you wire it in.

The public API isn't licensed for commercial use

VirusTotal's own API documentation states it "must not be used in commercial products or services", cannot substitute for antivirus products, and that non-compliance results in a permanent ban. This isn't a grey area or a rate-limit convention - it's the terms. There's a separate premium tier that is licensed commercially, and if VirusTotal's aggregated view is what you want, that's the route.

Submitted files don't stay private

VirusTotal's privacy policy says files submitted for scanning may be stored and shared with the anti-malware and security industry, and may be included in premium services offered to that industry.

In practice, premium subscribers can hunt across submissions with YARA rules - pattern-matching rules that search stored files for chosen strings - and retrieve what matches. If your uploads are customer documents, you're sending other people's data to a place designed to share it.

You don't have to take our word for either point. OWASP's File Upload Cheat Sheet mentions using public scanning services and adds its own caution: "beware of data leakage threats and information gathering by public services."

Architecture: three patterns and how each one fails

Whatever you decide about malware, the shape of the pipeline matters as much as the check inside it.

A software engineer reviewing an application's file upload pipeline at their desk
Most upload paths are built once, work fine and are never looked at again until something forces the issue.

Synchronous, inline

The request doesn't return until the file has been checked. Simple to reason about and simple to get right.

How it fails: it blocks. A large file or a slow scan holds a worker and eventually times out, and the failure mode under load is the whole endpoint. Fine for small files at modest volume, poor at either extreme.

Upload, validate, delete if bad

The file is written to its real destination, checked and removed if it fails. This is common because it's the smallest change to code that already works - and it contains a race condition PortSwigger documents clearly: for the milliseconds the file exists, an attacker who knows its URL can request it. If that location executes code, the check has already lost.

Don't build this one. It's listed because a lot of existing code is this pattern and it looks fine in review.

Quarantine, process, promote

The pattern worth building. The upload lands in a location nothing can reach and nothing can execute. A worker picks it up, processes it and either promotes it to the location it's served from or moves it aside. The file is never retrievable until it has passed.

The quarantine pattern: upload to a private store, scan or sanitise, then promote the clean file or isolate it
The file is never retrievable until it has passed. That single property is what the other two patterns give away.

A few details that decide whether it works in practice. The status belongs in your data model, not implied by which bucket the file is in - you'll want to show the user something while it's pending.

Failures need a dead-letter path, or files sit in limbo looking clean. And don't delete what fails: keep it somewhere safe, because the first thing you'll want after an incident is the artefact.

On serverless, watch the timeout. Scanning a large file inside a function with a hard execution limit is a well-known way to build something that works in testing and fails on the one file that matters.

Does your upload path need more than it has?

10 questions about your situation and your current controls. It'll give you a risk posture, the gaps worth closing first and an indication of which category of solution fits - including, quite often, "nothing more than you've got".

Your exposure
Your controls

Answer the questions above and the result updates below. Nothing you enter leaves your browser.


Answer all five dropdowns to see your result.

Your upload path

Files are stored inside the web root. That single fact outranks everything else here - it's the condition that turns an accepted upload into code your server will run. Moving storage out of the document root, or to object storage, changes this answer more than any other control on the list.

Nothing is checking uploads for malware, and the exposure is high. With this combination of who can upload, what you accept and who sees the files afterwards, the structural controls alone are carrying more weight than they can. The section on what you can build or buy covers the options, starting with the ones already in your cloud bill.

Worth closing first:

    Every structural control on this list is in place. That's the cheap half done properly.

    What fits your situation

    On what you've described, you probably need nothing beyond the structural controls above. Uploads are staff-only and nobody else ever sees the files, so there's little for a scanning or sanitisation service to earn. Spend the effort on the allow-list, generated filenames and storage that can't execute - then move on to a bigger risk.

    Your cloud platform's own scanning is likely enough. Azure's Defender for Storage malware add-on and AWS's GuardDuty Malware Protection for S3 are both agentless, and for document-sized files they cost a few pounds a month. If you accept images, re-encoding them on receipt is a strong extra control that costs nothing but CPU. Price it before you buy anything larger.

    A scanning step is the proportionate answer - either your platform's built-in scanning or a hosted scanning API, depending on whether you want to keep it inside your cloud account. Worth asking any provider where processing happens, what the billable unit is and what becomes of your file afterwards. If uploads later start coming from people outside your organisation, revisit this.

    This is the situation sanitisation is built for. You're taking documents from people you have no relationship with, or passing them on to others - so "not known to be malicious" is a weaker answer than you want. Rebuilding the file removes active content regardless of whether anyone has seen the payload before.

    Our CDR API does exactly this, processed in the UK, with published per-document pricing and no sales call. For how the technique works, start with the guide to content disarm and reconstruction.

    How this is scored: three of the six controls - the allow-list, content verification and generated filenames - count as critical, because their absence is what turns an upload path into a vulnerability rather than a weakness. Missing two of those reads red on its own. So does storing files inside the web root, whatever else is in place. This is editorial guidance to help you focus, not a security audit: it scores what you've told it, and it can't see your code.

    When you need none of this

    Worth saying plainly, because nobody selling file security will say it for you: a lot of upload paths don't need a scanning or sanitisation service, and buying one would be spending money on the wrong risk.

    If uploads are authenticated, internal, never served back to anyone else and never parsed by anything clever - a staff member attaching a receipt to an internal record, say - your exposure is small.

    What still earns its place is the cheap structural work: an allow-list, generated filenames, storage that can't execute, a size limit. All of that is code you write once, and it removes whole categories of problem without a subscription.

    Two things change the calculation. Strangers - the moment someone who hasn't signed up can put a file on your infrastructure, you're in a different situation, and PolyShell is what that looks like at scale.

    And onward reach - if a file gets served to another user, opened by a colleague or pulled into a document pipeline, the blast radius extends well past the upload endpoint.

    If neither applies, spend the money elsewhere. If both apply, keep reading.

    Where Red Eagle Tech fits

    We build software for a living and we run a sanitisation service, so it's fair to be clear about where each is relevant.

    Our CDR API does route 4 above. If your files actually arrive by email rather than through your application, our managed IT support applies the same technique to email and Microsoft 365 instead, as part of a managed stack.

    It rebuilds Office documents, PDFs and images rather than scanning them, over a REST API with OAuth2 client credentials - short-lived tokens rather than long-lived keys in headers. One call with ?wait=true returns the cleaned file for most documents; larger files fall back automatically to submit-and-poll. There are quickstarts in curl, C#, JavaScript and Python, and a live OpenAPI reference.

    Two things matter given what's above. Processing happens in the United Kingdom, on Azure UK South - the answer to the data-residency question the VirusTotal section raises.

    And your files are held transiently: submitted content is deleted once processing completes, cleaned files are kept for 24 hours and can be deleted sooner through the API, and we never use their content for anything else, including training models.

    You can sign up, add credit and make your first call entirely self-service if that suits you - you could be sanitising your first document within 10 minutes of starting. We hold the documentation to a standard that makes that realistic: good enough to hand to an AI coding assistant that has never seen the API and be up and running within a few minutes.

    Where we're the wrong choice. If you need an on-premises or air-gapped deployment, ICAP integration with an existing gateway (the protocol network appliances use to hand files to a scanner) or a product with defence accreditation, you want one of the enterprise platforms rather than us.

    And if all you need is a verdict on whether a file is known-bad, a scanning API will be cheaper per file than we are - your cloud platform may well be cheaper still. Those are real answers to real requirements, and we'd rather say so than sell you the wrong thing.

    Building something that accepts documents from people you don't know? See what the CDR API costs and how it works - published pricing, no sales call. For the technique itself, our guide to content disarm and reconstruction covers how it compares with antivirus, sandboxing and DLP, and our comparison of CDR tools covers who sells what and where each one is the wrong choice.

    Frequently asked questions

    You have four realistic routes. Run a scanner yourself, usually ClamAV, on infrastructure you maintain. Use what your cloud platform already offers - Microsoft Defender for Storage on Azure Blob, or GuardDuty Malware Protection for S3 on AWS - which is agentless and costs very little for document-sized files. Call a third-party scanning API. Or use a sanitisation API, which rebuilds the file instead of judging it. Which one fits depends on who can upload, whether those files get served back to other people and what you'd rather pay for.

    Often yes, and it's free to license - but it isn't free to run. Its signature database needs at least 1.6 GB of RAM resident and roughly double that briefly during updates, so a 4 GB instance is the practical floor. Size it to steady state and the daemon gets killed on signature reload. Add monitoring for the updater, because a silently stalled freshclam degrades detection without failing loudly. For a mail gateway or a low-traffic upload path on a team already running Linux hosts, it's a sound choice. For a latency-sensitive application path, price the instance and the attention before you call it free.

    Not the public one. VirusTotal's own API documentation states that the public API must not be used in commercial products or services, and that non-compliance results in a permanent ban. There's a separate premium tier licensed for commercial use. There's also a privacy dimension: VirusTotal's policy says submitted files may be stored and shared with the anti-malware industry, and premium subscribers can hunt across submissions with YARA rules and download matches. OWASP's own File Upload Cheat Sheet warns about data leakage to public scanning services for exactly this reason.

    No, and this is the single most useful thing to understand about upload validation. Signature checking is necessary and much better than trusting the extension or the Content-Type header - but it's defeatable. A polyglot file is valid in two formats at once, and the simplest ones are made by putting a GIF header in front of a payload. PortSwigger documents building polyglot JPEGs with ExifTool. In March 2026 the PolyShell flaw in Magento was mass-exploited using exactly this technique. Verify signatures, then decide separately what you're doing about malware.

    The framing is slightly wrong, which is why allow-lists beat deny-lists. Almost any format can carry something: Office files can hold macros, PDFs can hold JavaScript, SVG is XML and can hold script, archives can hide anything and even a valid image can carry an appended payload. The safer question is which types your application genuinely needs and what happens to them next. A file that's only ever stored and downloaded is a different risk from one that's parsed, rendered in a browser or served from your own domain.

    Possibly, and it's worth checking before you buy anything. Azure offers malware scanning as an add-on to Microsoft Defender for Storage, priced per storage account per month plus a charge per gigabyte scanned, for Blob Storage. AWS offers GuardDuty Malware Protection for S3, charged per gigabyte plus per thousand objects, with an ongoing free tier. Both are agentless with no infrastructure to run. For document-sized files the cost is genuinely small. Cloudflare has inline content scanning too, but it's Enterprise plan plus a paid add-on.

    Scanning compares a file against things known to be malicious and returns a verdict. Sanitising rebuilds the file from its valid components and discards everything else, so it never needs to recognise the threat. The practical consequence: a scanner can't tell you about a payload crafted an hour ago because there's nothing to match it against, whereas a rebuild removes anything that isn't legitimate document or image data regardless of whether anyone has seen it before. They answer different questions. Scanning answers is this known to be bad; sanitising answers is what's left safe to keep.

    Not anywhere your web server will execute code, and ideally not on your application server at all. Object storage - S3, Azure Blob, Cloud Storage - is the usual answer because it doesn't execute anything by design. If files must live on a filesystem, put them outside the document root and serve them through application code that sets an explicit content type and Content-Disposition. Also make sure the upload directory can't reconfigure how it's served: dropped .htaccess files that turn images into executable PHP were among the payloads observed in the wild in 2026.

    Store first, in a place nothing can reach, then process. Scanning inline before writing anything sounds safer but it blocks the request, times out on large files and creates the race PortSwigger documents: some applications write the file to its real location, validate it and delete it if it fails - and in the milliseconds it exists, an attacker can request it. The reliable pattern is quarantine, clean, promote. The file lands somewhere unreachable, gets processed asynchronously and only becomes retrievable once it has passed.

    Much less, and it's worth saying so plainly. If uploads are authenticated, internal, never served back to other users and never parsed by anything clever, your exposure is genuinely small and spending on a scanning or sanitisation service is hard to justify. The controls that still earn their place are the cheap structural ones: an allow-list, a size limit, storage outside the webroot and a serving path that can't execute. The calculation changes the moment strangers can upload, or staff uploads get shown to customers.

    Re-encoding an image is a genuinely good control and it's under-used. Decoding an image and writing a fresh one from the pixel data discards appended payloads, most metadata tricks and anything hiding in the container - it's sanitisation by another name for that one format. Two caveats. The image parser itself becomes attack surface, so keep the library patched. And it only covers images: it does nothing for the PDF, spreadsheet or archive sitting next to it in the same upload form.

    The smallest size that lets your users do the job. A limit isn't only about storage - it caps the cost of every downstream step, bounds decompression attacks and stops one request exhausting a worker. Enforce it in more than one place: at the edge or web server, at the framework and again at the point of processing, because a limit applied only after the file is fully buffered has already cost you the resource you were protecting. Also cap upload frequency per user and per IP.

    It's the formal name for sanitisation: taking a file apart, keeping the content and structure a valid file of that type is allowed to have, discarding everything else - macros, scripts, embedded objects, appended data - and building a fresh file from what's left. Because it doesn't try to recognise anything, it isn't affected by whether a payload is new. Our separate guide covers how it works, what it removes and how it compares with antivirus, sandboxing and DLP.

    Start with the category rather than the product, because the categories differ far more than the vendors within them. Decide first whether you need scanning, sanitising or nothing beyond what your platform already does, using your own exposure as the test: who can upload, what happens to the file afterwards and who sees it. Once you know the category, the questions that separate vendors are practical ones - where processing happens, what the billable unit actually is, whether you can buy without a sales call and what happens to your file after it's processed.

    Sources

    • OWASP - File Upload Cheat Sheet, OWASP Cheat Sheet Series (accessed July 2026). cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html
    • OWASP - A06:2025 Insecure Design, OWASP Top Ten 2025 (2025). owasp.org/Top10/2025/A06_2025-Insecure_Design
    • MITRE - 2025 CWE Top 25 Most Dangerous Software Weaknesses (2025). cwe.mitre.org/top25/archive/2025/2025_cwe_top25.html
    • MITRE - CWE-434: Unrestricted Upload of File with Dangerous Type; CWE-646: Reliance on File Name or Extension of Externally-Supplied File. cwe.mitre.org
    • PortSwigger - File uploads, Web Security Academy (accessed July 2026). portswigger.net/web-security/file-upload
    • Verizon - 2026 Data Breach Investigations Report (June 2026). verizon.com/business/resources/reports
    • AV-Comparatives - Malware Protection Test, March 2026 (April 2026). av-comparatives.org/tests/malware-protection-test-march-2026
    • Sansec - PolyShell: unrestricted file upload in Magento and Adobe Commerce (17 March 2026). sansec.io/research/magento-polyshell
    • Searchlight Cyber - Magento PolyShell: unauthenticated file upload to RCE (26 March 2026). slcyber.io/research-center
    • Wordfence - Attackers actively exploiting critical vulnerability in Ninja Forms File Upload, CVE-2026-0740 (April 2026). wordfence.com/blog
    • CISA - Vulnerability Summary for the Week of 13 July 2026, bulletin SB26-201. cisa.gov/news-events/bulletins/sb26-201
    • CISA - Known Exploited Vulnerabilities Catalog (accessed July 2026). cisa.gov/known-exploited-vulnerabilities-catalog
    • Cloud Security Alliance - Langflow path traversal: unauthenticated RCE actively exploited, CVE-2026-5027 (2026). labs.cloudsecurityalliance.org
    • Vaadata - File upload vulnerabilities and security best practices (accessed July 2026). vaadata.com/blog
    • VirusTotal - Getting started, API documentation; and Privacy Policy (accessed July 2026). docs.virustotal.com
    • Microsoft - What is Microsoft Defender for Storage, Microsoft Learn; and Defender for Cloud pricing (accessed July 2026). learn.microsoft.com
    • Amazon Web Services - Pricing and usage cost for Malware Protection for S3, GuardDuty documentation; and GuardDuty pricing (accessed July 2026). docs.aws.amazon.com
    • Cloudflare - Malicious uploads detection, WAF documentation; and WAF plan comparison (accessed July 2026). developers.cloudflare.com/waf
    • Arch Linux - ClamAV, ArchWiki (accessed July 2026). wiki.archlinux.org/title/ClamAV

    Getting the upload path right, once

    Most of what's above is code you write once and stop thinking about. The malware decision is the one that keeps costing you - either in infrastructure you maintain, or in a service you pay for, or in the incident you didn't plan for. If you'd like a second opinion on which of the four routes suits what you're building, we're happy to give you one.

    Ihor Havrysh - Software Engineer at Red Eagle Tech

    About the author

    Ihor Havrysh

    Software Engineer

    Software Engineer at Red Eagle Tech with expertise in cybersecurity, Power BI, and modern software architecture. I specialise in building secure, scalable solutions and helping businesses navigate complex technical challenges with practical, actionable insights.

    Read more about Ihor

    Sanitise the files, not the symptoms

    Rebuild every document that reaches your application, processed in the UK, over a REST API you can be live on this afternoon. Published per-document pricing, and no sales call to see it.

    See the CDR API

    Discovery call

    A friendly 15-minute video call with Kat to understand your needs. No preparation needed.

    • Discuss your project
    • Get honest advice
    • No obligation
    Kat Korson, Founder of Red Eagle Tech

    Kat Korson

    Founder & Technical Director

    Our team has 10+ years delivering software solutions for growing businesses across the UK.

    Send us a message

    Your information is secure. See our privacy policy.

    Find us