File Upload Vulnerabilities: Exploitation Techniques and Security Best Practices

file upload vulnerabilities

File upload features are ubiquitous in modern applications. Profile photos, invoices, identity documents and media all rely on mechanisms capable of accepting content provided by users or external systems. Behind this standard feature, however, lies a particularly broad attack surface.

In this article, we explore file upload vulnerabilities, detection methods and techniques for bypassing controls. We also detail the exploitations and impacts, as well as the measures to be implemented to prevent the risk.

Comprehensive Guide to File Upload Vulnerabilities

What is a File Upload Vulnerability?

A file upload vulnerability arises when an application allows files to be uploaded without applying sufficient controls over their validation, processing, storage or retrieval.

At first glance, the process seems simple: a user selects a document or an image and the application saves it. In practice, every uploaded file constitutes untrusted data and must be treated with the same level of scepticism as an HTTP parameter, a header or a value received via an API.

The difficulty stems from the complexity of files. Beyond their visible content, they may contain metadata, scripts, macros, interpretable code, embedded objects or structures specifically designed to cause unexpected behaviour in a parser. A file that appears harmless during the initial validation phase may therefore become dangerous when another component processes it later.

A vulnerability arises whenever an attacker can hijack this processing chain to carry out an action not intended by the application. Depending on the context, they may seek to get an executable file accepted, inject content interpreted by the browser, manipulate a storage path, trigger a vulnerability in a processing library, overwrite an existing object or consume server resources disproportionately.

This is why upload functionalities constitute a significant attack surface. A single request may involve the web server, the application framework, local or cloud storage, antivirus software, an image-processing library, a PDF parser, an OCR engine, a conversion solution or even a CDN. A weakness in just one of these components is sometimes enough to turn a legitimate business function into an attack vector.

The fundamental problem is therefore not simply that an application might accept a dangerous file extension. It lies in the fact that user-controlled content crosses several trust boundaries and that the controls applied are not necessarily consistent throughout its lifecycle.

How Does a File Upload Feature Work?

Before exploring bypass techniques, it is essential to understand what actually happens when a file is sent to a web application. Although the action may seem simple from the user’s perspective, an upload request often passes through several components responsible for receiving, validating, processing, storing and distributing the file. Each of these stages introduces new security assumptions.

Standard upload workflow

A standard upload begins when the user selects a file and submits it to the application. The browser then encapsulates the content in an HTTP request of the type ‘multipart/form-data’.

POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="document.pdf"
Content-Type: application/pdf

[file content]
------WebKitFormBoundary--

The request carries not only the bytes of the file, but also various pieces of metadata that can be controlled by the client, including the file name, the declared MIME type and any additional parameters. All these elements can influence the way in which the application validates and processes the received object.

The file does not usually pass directly from the browser to the final storage location. The web server first receives the request and forwards it to the application. The application then applies validation logic, selects a temporary or permanent destination, and may trigger further processing. It is common for a file to be subsequently scanned by antivirus software, resized, converted, indexed, subjected to OCR, stripped of certain metadata, or forwarded to a third-party service.

It is only after these operations that the content may be made accessible via a download URL, an API, a user area, a CDN or an internal workflow.

Security must therefore be in place throughout the file’s entire journey, not just at the moment it crosses the upload endpoint.

Where are security checks carried out?

A robust upload process combines several controls, as each one covers only part of the risk.

Processing stageTypical security checks
Client-side validationUsability checks only, size and file extension checks
Web server / gatewayRequest size limits, HTTP method restrictions, authentication routing
Application layerExtension allowlist, declared MIME type checks, naming policy, authorisation
Content validationMagic bytes, parser validation, antivirus, CDR
Processing servicesSandboxing, resource limits, outbound network filtering, parsers kept up to date
Storage and retrievalIsolation, access control, non-executable storage, secure HTTP headers

Validation of a file extension only checks the name. The declared MIME type provides information about what the client claims to be sending, but this value is itself determined by the user. Signatures allow a format to be identified more precisely, without guaranteeing that its entire structure is sound. Antivirus software, meanwhile, looks for known patterns, whilst storage controls determine who will be able to access the file once it has been accepted.

Security therefore depends on a combination of these mechanisms. An application that accepts ‘invoice.jpg’ because the file extension is permitted may then pass the bytes to a component that detects a different structure and interprets it in a way that the initial validation had never envisaged.

It is a common mistake to assume that a file becomes definitively ‘safe’ as soon as it has passed the first check. Each component that interacts with the content must apply safeguards appropriate to its own context of use.

How to Test a File Upload Feature?

An effective penetration test begins by modelling the functionality as a complete workflow, rather than immediately testing a list of payloads. It is necessary to identify what the application believes it is controlling, which component makes each decision, and whether components further down the chain interpret the same file in the same way.

The most reliable approach is to modify one property at a time. This method helps to highlight the application’s assumptions and avoids attributing a result to a single control when several parameters have changed simultaneously.

Map all upload and import points

The visible form is just one entry point amongst many. The auditor must identify profile photos, supporting attachments, document repositories, data imports, media libraries, CSV imports, archive ingestion, processing of attachments received via email, API endpoints, mobile features and mechanisms for importing a file from a remote URL.

Administration interfaces and back-end integrations are particularly important. They often handle richer file formats and may run with greater privileges than the features directly accessible to users.

For each entry, it is useful to note the authorised roles, the expected formats, the number of files accepted, the stated maximum size, the response returned, the existence of a finalisation endpoint, and how the file is published. In a cloud architecture, it is also necessary to determine whether the application actually receives the bytes or whether it merely generates a temporary token or a pre-signed URL allowing the client to send the object directly to storage.

Identify the baseline behaviour

Before changing anything, you must send a legitimate file and capture the entire exchange. A multipart/form-data request reveals several values that can then be tested independently: the name contained in Content-Disposition, the declared MIME type, the other form parameters and the binary body itself.

POST /api/files HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----Boundary
Cookie: session=...

------Boundary
Content-Disposition: form-data; name="file"; filename="report.pdf"
Content-Type: application/pdf

%PDF-1.7
[file content]
------Boundary--

The response may indicate that the file has been renamed, that an object ID has been generated, that a storage URL has been provided, or that deferred processing takes place after initial acceptance. A status API, unusual latency or the creation of derivative files may also help to identify secondary consumers.

Test validation layers independently

The most informative tests deliberately create inconsistencies between the file’s properties. The tester can keep the bytes whilst changing the file extension, retain the file extension and change the Content-Type, preserve an expected signature whilst altering the rest of the content, or provide a syntactically valid file that contains unusual structures.

Comparing the immediate response and subsequent behaviour helps to determine which property is actually used by each component. Browser-side restrictions should be regarded as usability checks rather than a security boundary, as it is always possible to replay the HTTP request directly.

Server-side controls must then be tested for normalisation issues, incomplete blocklists, parsing differences, inconsistent size limits and discrepancies between the upload endpoint and the service that processes the file later.

Track storage, retrieval and processing

Once the file has been accepted, you need to understand where it is sent. Does the application return a URL? Is the content served directly by the web server, via an application endpoint, from a CDN or from object storage? Is the original filename retained? Is the MIME type recalculated or simply taken from the request? Is the file displayed inline or forced to download? Is the storage directory executable by the web server?

Secondary processing tasks must be triggered whenever the functionality allows: thumbnail generation, document previews, OCR, archive extraction, metadata reading, conversion, antivirus scanning or indexing. Robust input validation does not protect against a vulnerable or over-privileged worker located further down the chain.

Assess access controls and multi-tenant isolation

File security also relates to the operations permitted on the object. You must test creation, reading, replacement, renaming, sharing and deletion from accounts with different roles and, where applicable, belonging to different organisations or tenants.

A common scenario is that of a properly secured upload endpoint followed by a download or replacement endpoint that accepts a user-controlled identifier without verifying authorisation for the target object.

GET /api/files/8d7d7db0 HTTP/1.1
Host: example.com
Cookie: session=userA

# Repeat the request with the ID of a file belonging to userB
# or to another tenant. Authorisation must be checked for each object.

Thumbnails, converted versions, previews and cached copies must inherit exactly the same authorisation model as the original file. A publicly accessible derivative copy is sufficient to circumvent proper protection on the original.

Testing asynchronous processes and secondary consumers

Modern architectures often accept a file before processing it via a message queue or a worker. Several intermediate states then arise: pending, being analysed, rejected, converted or published. It is essential to ensure that a file is neither retrievable nor executable until all mandatory checks have been completed.

It is also necessary to check what happens in the event of a failure. Is a rejected file actually deleted from all temporary areas? Does an interrupted conversion leave an accessible artefact? Is a download URL generated before the virus scan has finished? These transient windows can pave the way for race conditions.

Finally, the penetration test must take indirect users into account. A document may later be opened by a back-office user, indexed, imported into an ERP system, sent by email or copied to a secondary storage location. These pathways can transform seemingly innocuous metadata or values into stored XSS, formula injection, parser exploitation or exfiltration channels.

Techniques for Bypassing File Upload Controls

Once the workflow has been understood, the aim is to determine how the controls react to unexpected data. There is no one-size-fits-all technique: the attacker first seeks to establish whether the application relies on the filename, the file extension, the MIME type, magic bytes, the file structure, the storage path or the point at which validation takes place.

The general principle is to create a discrepancy between what is checked and what is subsequently interpreted. Each test should, as far as possible, isolate a single hypothesis in order to understand exactly where the weakness lies.

Bypassing extension validation

Extension validation is one of the most common checks. For example, an application may accept avatar.jpg, invoice.pdf or document.docx, whilst rejecting script.php, payload.jsp or shell.aspx.

This mechanism is useful, but it becomes vulnerable when implemented using a simple string comparison. The name sent in the Content-Disposition header is entirely controlled by the client and can be altered before it reaches the server.

Content-Disposition: form-data; name="file"; filename="document.pdf"
Content-Type: application/pdf

If the decision is based solely on this value, the auditor may test different representations of the name in order to identify validation inconsistencies.

Double extensions

A double extension involves adding a permitted extension after a potentially dangerous extension.

file.php.jpg
document.jsp.png
report.aspx.pdf

This technique is aimed at implementations that check for the presence of a permitted extension anywhere in the name, rather than extracting and strictly checking the final suffix.

if (filename.includes(".jpg")) {
  allowUpload();
}

file.php.jpg is then accepted because the string contains .jpg. A robust implementation must first normalise the name, extract the final extension according to an unambiguous rule, and then compare it against a strict allowlist.

Double extensions become particularly relevant when multiple components apply different rules. The application layer may examine only the final suffix, whilst a web server, reverse proxy, converter or legacy handler interprets the name differently. It is therefore necessary to check not only whether the file is accepted, but also how the final object is processed.

Alternative executable extensions

A blocklist that only blocks .php, .asp or .jsp files may overlook other file extensions that are interpreted by the platform or by a specific configuration.

TechnologyExamples of file extensions that may be interpreted or executed
PHP.php, .php3, .php4, .php5, .phtml, .phar, depending on the configuration
ASP.NET.aspx, .ashx, .asmx, .ascx
Standard ASP.asp and certain legacy file extensions such as .asa or .cer
Java.jsp, .jspx, archives that can be deployed as .war files in certain administration workflows
ColdFusion.cfm, .cfml, .cfc

An application can therefore block file.php whilst allowing file.phtml if that variant has not been added to the blocklist. This is precisely why a list of explicitly permitted formats is preferable to listing all formats considered dangerous.

Handling case sensitivity

A case-sensitive comparison can lead to inconsistent behaviour.

file.php
file.PHP
file.PhP
file.pHp

A check like this is unreliable if the value is not normalised first.

if (extension === "php") {
  rejectUpload();
}

The correct approach is to convert the extension to a canonical representation before making any decision.

const extension = getExtension(filename).toLowerCase();

This bypass is rarely sufficient on its own in modern frameworks, but it remains useful for detecting custom or inconsistent validation rules.

Full stops and spaces at the end of names

Full stops or trailing spaces can cause discrepancies between the application logic and the file system.

avatar.jpg.
avatar.jpg 
file.php.
file.php 

Depending on the operating system, framework or storage layer, certain characters may be removed or normalised. The name checked by the application may therefore differ from the name actually stored or interpreted.

You also need to be wary of regular expressions or poorly anchored string comparisons that tolerate spaces, encoded characters or subsequent transformations. A robust policy must decode, remove unnecessary characters, normalise and then canonise the name before making a decision.

Null byte injection

Null-byte injection is a long-established technique that targets components which treat the null character as a string terminator.

file.php%00.jpg

The application may interpret the file as a .jpg, whilst a native component or an older library interprets the name as file.php. Modern environments generally guard against this behaviour, but the technique remains relevant when an application incorporates native code, legacy extensions or older libraries.

Beyond direct exploitation, this test primarily serves to verify that control characters and encodings are rejected before the filename is validated.

Unicode and canonicalisation issues

Unicode introduces variants that are visually similar but technically distinct. An attacker could exploit homoglyphs, invisible characters or different forms of normalisation.

avatar.jpɡ
avatar.jg
avatar.jpg

The risk arises when one component normalises the string whilst another does not. A representation may pass validation only to be transformed before storage, display or processing.

A secure naming policy defines the characters that are actually required, applies consistent Unicode normalisation, enforces a maximum length and rejects invisible or ambiguous characters where there is no business justification for them.

Bypassing MIME type validation

The MIME type contained in a multipart request is provided by the client and can be changed at will.

Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain

An attacker can simply replace the value with:

Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: image/jpeg

If the application accepts the second submission solely because the Content-Type is set to image/jpeg, it is relying on unreliable metadata. The declared MIME type remains useful as an indicator of consistency, but it must be cross-referenced with the expected file extension, the binary signature, the file structure and the intended use of the file.

Bypassing signature validation and forging magic bytes

Magic bytes are the first bytes used to identify many file formats.

File typeCommon signatures / magic bytes
JPEGFF D8 FF
PNG89 50 4E 47
GIF47 49 46 38
PDF25 50 44 46
ZIP50 4B 03 04

This check is more robust than a MIME type declared by the client, but it does not, on its own, guarantee that the entire file is valid or safe. Content may begin with a correct signature and then contain unexpected data, active objects or a structure intended for a different parser.

The issue lies in the distinction between identification and safety. Magic bytes allow us to confirm that a file resembles a particular format, not that it contains no dangerous behaviour. For sensitive formats, validation must rely on an up-to-date parser, reject malformed or ambiguous structures and, where possible, reconstruct or re-encode the content into a canonical representation.

Parsing differences and multilingual files

A parsing discrepancy arises when two components assign different meanings to the same bytes. A multilingual file illustrates this principle particularly well: it is constructed in such a way that it can be accepted or interpreted meaningfully by several parsers.

Processing does not always require a file to be perfectly valid according to two specifications. Sometimes it is sufficient for one validator to tolerate a particular structure or additional bytes, whilst another component extracts a second, usable interpretation. Historical examples include GIF/JAR and PDF/ZIP formats, and certain image formats remain perfectly readable despite the addition of data at the end of the file.

During an audit, the key question is therefore not merely ‘what type of file is this?’, but ‘which parsers will process it, and do they agree on its contents?’. The upload validator, the preview engine, the web server, the metadata extractor and the converter may all behave differently.

Injections via filename

The original name becomes dangerous when it is reused in a different context without being properly processed. This value may appear in an HTML page, a log, a shell command, an SQL query, an HTTP header or a local path. Each of these contexts has its own encoding or formatting rules.

Content-Disposition: form-data; name="file"; filename="invoice.pdf"

# Examples to be tested only in an authorized environment:
"><svg onload=alert(1)>.jpg
../../invoice.pdf
report;id;.pdf
invoice%0d%0aX-Test: injected.pdf

Displaying the name without HTML encoding may lead to a stored XSS. Inserting it into a shell command may result in a command injection. Placing it without validation in the Content-Disposition header or another header may cause header injection issues in certain software stacks.

The safest approach is to generate an internal identifier for storage and to retain the original name solely as metadata. Even in this case, this metadata must be normalised, limited in length and encoded according to the context in which it will be displayed or returned.

Path traversal during upload

A path traversal occurs when user-controlled data directly influences the location where the file will be written. A vulnerable implementation may concatenate the supplied name with a destination directory without resolving and validating the final path.

const destination = "/var/www/uploads/" + filename;

// Typical patterns to test:
../
..\\
%2e%2e%2f

If these sequences are accepted, an attacker may attempt to move outside the intended directory and write to another location accessible to the application account. The same risk may exist when the name itself is replaced, but the user controls a folder, a tenant ID or an import path.

The reasoning is different in object storage such as Amazon S3. Keys are not filesystem paths, and `../` does not allow one to ‘exit’ a bucket as it would on a local disk. The relevant issues instead concern insufficient control over keys and prefixes, collisions, object overwriting, isolation between tenants, or the possibility of placing an object in a namespace used by another component.

On a file system, the application must construct its paths from server-side-generated identifiers, normalise the final destination and verify that it remains within the authorised root. In object storage, it must validate the bucket, prefix and key on the server side and link them to the authenticated identity.

Changing a server configuration via uploaded files

Some web servers allow per-directory configuration files that can alter the way objects within a given area are interpreted. If the user is free to choose the file name and the upload ends up in a directory where these mechanisms are active, a seemingly harmless file extension may become executable once the configuration has been modified.

Typical examples include .htaccess on certain Apache configurations or web.config in IIS environments. Exploitability depends entirely on the server and its rules, but these files are important to test when an application relies primarily on a blocklist of dynamic file extensions.

A secure architecture prevents the user from choosing the physical filename, places uploads outside executable directories and treats uploaded content as data, never as configuration.

Archives require special handling, as validating the external container reveals almost nothing about the files that will appear once they have been extracted. A service may correctly identify a ZIP or TAR file whilst still being vulnerable to malicious paths, special links, extreme compression ratios or nested formats.

Zip Slip and Path Traversal in the archives

Zip Slip is a form of arbitrary write caused by an archive entry whose name contains a directory traversal or an absolute path. A vulnerable extractor concatenates the destination with the entry name and writes the result without checking the canonical path.

archive.zip
  documents/report.txt
  ../../public/config.txt

# An insecure extraction may write the second entry outside the intended directory.

This issue does not only affect ZIP files. TAR, JAR, WAR, CPIO, RAR, 7z and other formats may also contain path information. For each entry, the application must resolve the destination and then check that it remains within the extraction root directory before writing anything.

Some archives may contain symbolic links, hard links or special file system objects. Even if the ../ sequence is disallowed, a link created in the extraction directory could cause a subsequent write operation to point to an area outside the sandbox.

If the product has no functional reason to support these objects, the best policy is to reject them. Otherwise, the extraction engine must explicitly define and enforce a secure policy for managing links.

Decompression bombs and resource amplification

The compressed size is not a reliable measure of a file’s actual cost. An archive of a few megabytes may expand to tens of gigabytes, contain millions of small entries, involve a high level of nesting, or trigger very resource-intensive parsers.

An audit must therefore take into account the expected decompressed size, the compression ratio, the number of entries, the nesting depth, the extraction time and the resources consumed. The same reasoning applies to images of extreme dimensions, XML content capable of increasing processing times, videos requiring heavy transcoding, or complex documents.

HTTP PUT and WebDAV exploits

Not all methods of writing data involve an HTML form. Some environments allow files to be written via PUT, particularly when WebDAV is enabled or a server is incorrectly configured.

PUT /uploads/test.txt HTTP/1.1
Host: example.com
Content-Type: text/plain
Content-Length: 12

test content

If the server accepts unauthenticated or insufficiently restricted PUT requests, an attacker can write directly to an accessible location without going through the usual application logic. Any file extension, MIME type, antivirus or storage checks implemented within the application are then completely bypassed.

This type of vulnerability usually stems from server configuration: an exposed WebDAV service, a permissive reverse proxy rule, a forgotten development endpoint, or overly broad write permissions. Unnecessary HTTP methods must be disabled, and public directories must not become areas where arbitrary writing is permitted.

Race conditions during upload

Race conditions target the period between a file being written, validated, processed and, where applicable, deleted. Some applications store the file temporarily before the scan or analysis is complete. If the file is accessible during this window, an attacker may attempt to exploit it before it is quarantined or deleted.

1. The file is uploaded.
2. It is stored temporarily.
3. The validation or virus scan begins.
4. The file is accessible for a short period.
5. The application deletes or blocks it if the check fails.

The weakness does not lie in the validation rule, but in the point at which the content becomes accessible. The risk increases when processing is asynchronous and a URL is returned immediately whilst the scan or conversion is taking place in the background.

A secure design keeps new files in a quarantine zone inaccessible to users and interpreters until all checks have been successfully completed. The same problem can arise with imports from a URL: the system first downloads the content, creates metadata or a temporary resource, and only then completes its validation.

Exploiting File Upload Vulnerabilities

Bypassing an upload check is only the first step. The actual impact depends on what the application, server, browser or a third-party component does next with the accepted file.

In most serious scenarios, the file becomes exploitable because another component assigns it an active meaning. The browser may interpret an HTML or SVG document, the web server may execute a script, a conversion engine may parse a PDF, an XML parser may resolve external entities, and a worker may download resources referenced by the content. Understanding the exploitation therefore requires tracking the file beyond the initial upload.

Stored XSS via SVG and HTML files

SVG files are not simply raster images. They are based on XML and may contain interactive features when interpreted as documents. Allowing .svg or .html files can therefore lead to a stored XSS vulnerability if user-controlled content is subsequently rendered in a context that allows scripts to be executed, particularly when it is served from the same origin as the main application.

const allowedExtensions = ["jpg", "jpeg", "png", "gif", "svg"];

app.post("/upload", upload.single("file"), (req, res) => {
  const extension = req.file.originalname.split(".").pop().toLowerCase();

  if (!allowedExtensions.includes(extension)) {
    return res.status(400).send("Invalid file type");
  }

  res.send("File uploaded successfully");
});

However, the impact depends on the exact rendering context. Modern browsers impose significant restrictions when an SVG is loaded solely as an image, for example via a tag: in this context, the execution of scripts and the loading of certain external resources are generally blocked. These restrictions do not apply in the same way when the SVG is opened as a standalone document or embedded via <object>, <iframe> or <embed>. An SVG injected inline into an HTML document constitutes yet another context.

This distinction is fundamental during a penetration test. One must not assume that simply uploading an SVG automatically results in an XSS vulnerability. The tester must verify the actual URL, the Content-Type and Content-Disposition headers, the origin, the embedding mode and the browsing context. Where the SVG is not essential to the business, converting images to a trusted raster format such as PNG or JPEG significantly reduces the attack surface. If it is necessary, its content must be sanitised using a specialised library and served in a restricted context, ideally separate from the main origin.

The same principle applies to HTML, XHTML or other formats that can be interpreted directly by the browser. Storage classified as ‘static’ is not sufficient to render content harmless if the browser ultimately treats it as an active document using cookies, the origin or the application’s privileges.

Remote code execution via webshell uploads

Remote code execution (RCE) can occur when an uploaded file is stored in a location where the web server or another layer interprets it as code.

A very permissive PHP handler might, for example, look like this:

<?php
$uploadDir = __DIR__ . "/uploads/";
$targetPath = $uploadDir . basename($_FILES["file"]["name"]);

move_uploaded_file($_FILES["file"]["tmp_name"], $targetPath);

echo "File uploaded successfully";
?>

This implementation has several weaknesses: it retains the name provided by the client, does not validate the file type, writes to a directory accessible via the web, and does not prevent the storage of executable content. If the server is configured to interpret PHP in this directory, a .php file can be executed when a user subsequently accesses its URL.

The same reasoning applies to other technologies. Depending on the configuration, ASP.NET, JSP, PHTML or other dynamic formats may be interpreted. The risk therefore does not depend solely on PHP, but on the relationship between the accepted file type and the handlers active on the storage path.

The exploitation chain relies on two distinct conditions: the attacker must succeed in placing an executable file, and a second event must trigger its execution. Preventing either of these is sufficient to significantly reduce the risk. A strict allowlist reduces the ability to place code, whilst non-executable storage outside the web root prevents the server from interpreting the content even if validation fails.

Malicious handling of PDFs and documents

PDFs, Office documents, images and media are often dangerous not because the web server executes them directly, but because the application automatically parses or transforms them. Generating previews, OCR, metadata extraction, thumbnail creation and format conversions increase the attack surface by exposing complex libraries to user-controlled bytes.

const { exec } = require("child_process");

app.post("/upload-pdf", upload.single("file"), (req, res) => {
  const inputPath = req.file.path;
  const outputPath = `/tmp/previews/${req.file.originalname}.png`;

  exec(`convert ${inputPath} ${outputPath}`, (error) => {
    if (error) return res.status(500).send("Preview generation failed");
    res.send("PDF uploaded and preview generated");
  });
});

This approach introduces two categories of risk. The first stems from the parser itself: a vulnerability in ImageMagick, Ghostscript, LibreOffice, ExifTool, FFmpeg or one of their delegates may be exploitable via a specially crafted file. The second is independent of the format: if a user-controlled name or path is interpolated into a shell command, the application may become vulnerable to command injection.

A process-launching API that takes arguments separately is preferable to constructing a command as a string.

const { spawn } = require("child_process");

spawn("convert", [inputPath, outputPath], { shell: false });

This change reduces the risk of shell injection, but does not make the conversion engine inherently secure. The component must remain isolated within a sandbox, with a non-privileged identity, strict CPU and memory limits, a minimal file system, no unnecessary sensitive information, and heavily restricted outbound network connections.

A distinction must also be made between server-side risks and those affecting the person opening the document. A PDF or Office document may contain links, embedded objects, macros or other active content. Whether these are executed depends on the format, the reader and its configuration; however, a public upload platform can become a distribution channel for malware or phishing attacks even if the server itself is never compromised.

XML attacks via uploaded files

XML attacks occur when uploaded content is processed by a parser configured to allow dangerous features.

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();

Document document = builder.parse(uploadedFile);

Depending on the language, the library and its version, the default configuration may allow document type declarations or the resolution of external entities. A document controlled by an attacker could then cause the parser to access resources that should never have been exposed.

A more secure configuration explicitly disables features that are not required.

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);

DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(uploadedFile);

This example illustrates why upload validation alone is not sufficient. A file may have a permitted extension whilst still exploiting the parser responsible for processing it.

XML may also be embedded within a container format rather than being received directly with the .xml extension. Modern Office formats, SVG and many application formats use XML internally. The audit must therefore identify which internal files are actually parsed and under what configuration.

SSRF via file processing

An SSRF may occur when a file contains references to external resources that the server automatically loads during processing.

For example, an application may accept an HTML or SVG document and convert it into an image or a PDF using a headless browser.

app.post("/render", upload.single("file"), async (req, res) => {
  const html = fs.readFileSync(req.file.path, "utf8");

  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  await page.setContent(html, {
    waitUntil: "networkidle0"
  });

  await page.pdf({ path: "/tmp/output.pdf" });

  res.send("File rendered successfully");
});

If the content contains a remote resource, the renderer may attempt to retrieve it from the server’s network.

<img src="https://attacker-controlled.example/image.png">

In a vulnerable environment, the same mechanism can target internal services or link-local destinations that are inaccessible directly from the Internet. The vulnerability lies not in the upload itself, but in the fact that user-controlled content influences an outbound request made by a trusted component.

Protection involves restricting workers’ network connections, blocking private and link-local ranges when they are not required, disabling the loading of external resources, and running the renderer in an isolated environment.

A related scenario involves features such as ‘upload from a URL’, ‘import an image’, ‘generate a preview’ or certain converters. In this case, the client does not provide the bytes directly: it provides a URL that the server will download. The component must therefore be audited as a fetcher that is potentially vulnerable to SSRF, with checks on redirects, protocols, successive DNS lookups and access to internal destinations.

Denial-of-service attack via file upload

An upload feature can be exploited to exhaust storage, memory, CPU, bandwidth or worker capacity. The risk is not limited to sending a single enormous file. An attacker could upload a large number of medium-sized files, choose formats that are resource-intensive to parse, provide images of extreme dimensions, create highly nested documents or trigger numerous conversions.

from PIL import Image
from flask import Flask, request

app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 5 * 1024 * 1024

@app.route("/upload", methods=["POST"])
def upload():
    file = request.files["file"]
    image = Image.open(file)
    image.thumbnail((500, 500))
    image.save("/tmp/resized.jpg")
    return "Image processed"

The five-megabyte limit restricts the size of the request, but it does not limit either the image dimensions or the memory used during decoding. Nor does it control the number of requests, job concurrency, the size of derived files or the cost of subsequent processing.

An effective defence imposes limits at several levels: size per file and per request, number of files, dimensions, media duration, number of archive entries, decompressed size, nesting depth, processing time, memory, CPU, temporary disk space and number of concurrent jobs. Per-user or per-tenant quotas and rate-limiting mechanisms prevent a single entity from monopolising resources.

Broken access controls, disclosure and overwriting of files

A feature may be vulnerable even when the accepted bytes are completely harmless. A faulty authorisation check may allow a user to retrieve, replace, rename or delete another user’s file. In a multi-tenant application, the same weakness may expose documents belonging to another organisation.

Predictable identifiers, direct URLs to object storage and derived files are common sources of inconsistencies. An application may correctly protect GET /documents/{id} whilst publicly exposing /thumbnails/{id}.png, or verify the owner upon creation without repeating the check when a PUT endpoint replaces the file.

PUT /api/files/5f31c9d2 HTTP/1.1
Host: example.com
Cookie: session=attacker
Content-Type: application/pdf

[replacement file]

# The server must verify that the authenticated user is authorised
# to modify this specific object, and not merely that they have a session.

Overwriting can also result from a collision of names or shared storage keys. The problem becomes particularly critical if the object being overwritten is a template, a configuration file, a static asset, a package or a document automatically used by a trusted service.

Arbitrary file writing via archive extraction

When an uploaded archive is extracted on the server side, an entry containing a path traversal can turn an import function into an arbitrary write operation with the privileges of the extraction worker.

The impact depends on what this component is able to modify. Overwriting an application file, a scheduled task, a configuration, an SSH key, a plugin or a web-served asset can escalate a simple extraction vulnerability into code execution or persistence.

The audit must therefore check the extraction root, canonical path checks, absolute paths, drive letters on Windows, symbolic links, name duplicates and the overwrite policy. The danger lies not in the archive having passed validation, but in the write operation carried out after it is opened.

Formula Injection and active Office content

CSV imports and spreadsheets pose a different risk. A value beginning with a formula prefix may be interpreted as a formula when the resulting file is opened in a spreadsheet programme. The behaviour depends on the software and its configuration, but the security boundary is breached when untrusted text is no longer treated as data but as an expression to be evaluated.

name,department,comment
Alice,Finance,"=1+1"

# During a security test, use only non-destructive expressions
# to check whether imported or exported values are evaluated.

The risk may arise during import, but also during export. An application may store a value provided by a user, treat it as a perfectly normal string, and then export it later to a CSV file opened by a colleague. This is therefore a secondary scenario: the dangerous context arises after the data has been stored.

Office files containing macros or embedded objects raise a related issue concerning the distribution of active content. If these functions are not necessary, the formats in question can be rejected or reconstructed using CDR. If they are essential, the documents must be explicitly treated as untrusted and must not be opened automatically by privileged processes.

Abuse of object storage and pre-signed URLs

Many modern applications send files directly from the browser or mobile app to object storage. The application server first authorises the operation and then returns a temporary token or a pre-signed URL. The client then transmits the bytes to the storage service without routing them through the main web server.

Whilst this architecture may be secure, it shifts a crucial part of the security burden to the token generation process, the choice of object key, the bucket policy and the validation carried out after the upload.

PUT /tenant-a/uploads/2b7f... HTTP/1.1
Host: storage.example
Content-Type: application/pdf

[file content]

# The upload capacity must be linked to a bucket and a key chosen on the server side,
# with a short validity period and the operation strictly limited to what is necessary.

The pentester must determine whether the user can influence the bucket, prefix or key; whether the URL can overwrite an existing object; whether it can be reused; whether content constraints are actually enforced; and whether the object becomes public before server-side validation.

With Amazon S3, for example, an upload carried out via a pre-signed URL to a key that already exists replaces the corresponding object. Predictable or client-chosen keys can therefore create an integrity issue even when the URL’s signature is perfectly valid.

A pre-signed URL should be treated as a bearer-type credential. Logging, a short validity period, server-side generated keys, minimal permissions and a quarantine-to-publication workflow are more important than obscuring the URL itself.

How to Prevent File Upload Vulnerabilities?

No single control can fully secure an upload function. An attacker could target the validation of the filename, the file type, its internal structure, storage, asynchronous processing, the rendering component or access controls.

A robust architecture therefore relies on several independent layers. Each mechanism must mitigate a specific category of risk and reduce the impact of a failure occurring elsewhere in the chain.

Only allow the necessary file types

The first question is not ‘which dangerous file extensions should be blocked?’, but ‘which file formats are actually necessary?’. A restricted allowlist automatically reduces the number of parsers, rendering contexts and active features that the application must support securely.

Whilst a profile picture can be displayed as a JPEG or PNG, allowing SVG, PDF, HTML and arbitrary archives introduces risks without offering any functional benefit. The policy must therefore be defined on the basis of business requirements, rather than a list of formats known to be malicious.

const allowedExtensions = ["jpg", "jpeg", "png", "pdf"];

const extension = getExtension(filename).toLowerCase();
if (!allowedExtensions.includes(extension)) {
  rejectUpload();
}

The check must take place after the name has been decoded and normalised, and must be accompanied by explicit limits on length and permitted characters. A deny list may still be useful as an additional safeguard, but it must never constitute the primary security measure.

Validate MIME types, signatures and file structures

The Content-Type sent by the client is a verifiable piece of metadata and does not prove anything about the actual content. Binary signatures provide a more reliable indicator, but checking only the first few bytes does not validate the entire structure.

A robust validation process compares the expected file extension, the declared MIME type, the detected signature and, for high-risk formats, the result of a specialised parser.

import { fileTypeFromFile } from "file-type";

const type = await fileTypeFromFile(uploadedFile);
if (!["image/jpeg", "image/png"].includes(type.mime)) {
  rejectUpload();
}

For images, decoding and then re-encoding into a new file helps to remove many ambiguities, data added at the end of the content, and certain unnecessary structures. For documents, a CDR solution or a specialised library may be more suitable.

Validation must fail safely when the format is ambiguous, malformed or unsupported. The file must remain in quarantine until all mandatory checks have been completed.

Authenticate and authorise uploading, downloading and editing

Only users who genuinely need the feature should be able to use it. Crucially, authorisation does not end once the file has been uploaded. Every instance of reading, replacing, renaming, sharing or deleting must verify the rights for that specific object.

Thumbnails, previews and converted versions must inherit the same protection model. A single public derivative copy can, on its own, circumvent correct authorisation on the original.

In a multi-tenant environment, the tenant ID used in the database, paths or object keys must be derived from the server-side authenticated context, not from a parameter provided by the client. A file must be public because the product has explicitly decided so, not because a bucket or URL is too permissive.

Generate file names and storage keys on the server side

The name provided by the user must not become the actual name used for storage. The application must generate an unpredictable identifier, such as a UUID, and retain the original name solely as display metadata.

import crypto from ‘crypto’;

const storageId = crypto.randomUUID();
const storedFilename = `${storageId}.pdf`;

// The original filename is retained only as validated metadata.
const originalFilename = sanitiseDisplayName(req.file.originalname);

This strategy reduces collisions, the potential for path manipulation, and the use of names that have special significance for the operating system or web server. If the extension must be retained for operational reasons, it must be derived from the server-side validated type and not blindly copied from the request.

The same rule applies to object storage keys: they must be constructed from a server-controlled tenant prefix and a generated identifier, not directly from a name or path chosen by the client.

Store files outside executable web locations

A common architectural flaw is to write uploads directly to a directory that is accessible to and can be parsed by the web server, for example /var/www/html/uploads/. A single validation error can then lead to code execution.

Risky:     /var/www/html/uploads/document.pdf
Safer:   /var/app/storage/uploads/7c8f9a4e
Retrieval: GET /download/7c8f9a4e

It is preferable to use separate storage or a directory outside the web root. The content can then be served via a controlled application endpoint or a properly configured CDN/storage mechanism, which enforces authentication, authorisation, rate limiting, secure HTTP headers and logging.

If public distribution is required, a separate origin that is unable to execute server-side code and has limited permissions reduces the risk of user content being treated as a trusted application resource.

Apply the principle of least privilege

There is no universal chmod setting that makes an upload directory secure. The correct permissions depend on the operations required by each component. The aim is to grant each service only the rights it needs and to remove any ability to execute user content where this is not explicitly required.

An ingestion service may need to write to a quarantine directory. An antivirus worker may only need read access to this area and write access to an output directory. The component that serves published files may be restricted to read-only access.

The main web server must not automatically inherit write access to all processing or publishing zones. The same principle must apply to cloud IAM roles, bucket policies and the credentials used by workers.

Set limits on size, number, quotas and processing

A query size limit is necessary, but it only covers one aspect of the risk. It is also necessary to limit the number of files per query, the storage quota per user or tenant, the frequency of uploads, the number of concurrent jobs, image dimensions, media duration, the number of archive entries, the expansion rate, the nesting depth and the maximum processing time.

These constraints must be applied as early as possible, before resource-intensive operations. Workers must also have explicit limits on CPU, memory, temporary disk space and execution time. Temporary directories require quotas and reliable clean-up, particularly following errors.

When a process decompresses or transforms a file, the output size must also be controlled. A request for five megabytes may result in several gigabytes of data after decompression or conversion.

Secure extraction of the archives

Extraction must take place in an isolated, non-executable temporary directory that contains no sensitive data. For each entry, the application must determine the final path and then verify that it is indeed located within the authorised root directory before writing any data.

Absolute paths, traversals, special files and symbolic links or hard links must be rejected unless there is an explicit business requirement accompanied by a secure implementation. Limits on the number of entries, the total decompressed size, individual file size, compression ratio, depth and extraction duration must be independent of the limits applied to the HTTP request.

The behaviour in the event of duplicate filenames must also be clearly defined. An extraction that silently overwrites an existing file may create an exploitable integrity vulnerability. Finally, each extracted file must undergo the same type, content and malware checks as a file uploaded directly before being published or consumed by another system.

Serve uploaded files securely

The response itself acts as a security check. The server must return a Content-Type determined by a trusted validation process, rather than simply copying the MIME type provided by the client.

X-Content-Type-Options: nosniff instructs the browser to respect the declared type rather than attempting to infer a different format. When content does not need to be rendered inline, Content-Disposition: attachment reduces the likelihood of an executable file being interpreted as a web page in the browser.

HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="report.pdf"
X-Content-Type-Options: nosniff
Cache-Control: private, no-store

For highly untrustworthy user-generated content, a separate origin accompanied by a restrictive security policy can mitigate the impact of same-origin behaviour if an HTML, SVG or similar file is accidentally parsed. Authentication, caching and access control must, however, remain properly designed to ensure that this separate origin does not become a source of private document leaks.

Secure object storage and workflows using pre-signed URLs

In a direct upload to the cloud, the application must generate the destination bucket and key on the server side and link them to the authenticated user or tenant. The principal generating the pre-signed authorisation must have the minimum necessary permissions, and the validity period must be kept short.

Generic permissions that allow the client to freely choose a key, prefix or operation must be avoided. Write capability on a namespace that is too broad transforms a limited feature into a more extensive object-modification primitive than intended.

The newly uploaded object must be treated as being in quarantine. A callback or application-level finalisation call can verify its existence, check its size and metadata, initiate server-side validation and scanning, and only then mark the object as published or move/copy it to a distribution area.

ACLs or bucket policies must not publicly expose the file before this state is reached. Where overwriting would be dangerous, unique keys must be generated, and mechanisms such as versioning or conditional operations can provide additional protection.

It is useful to log both the creation of the pre-signed URL and the corresponding storage event in order to link a suspicious upload to the application identity that authorised it.

Protecting upload endpoints against CSRF

When an application authenticates sensitive actions using credentials automatically sent by the browser – such as session cookies – the upload must benefit from the same CSRF protections as other state-changing operations.

Depending on the architecture, this may involve synchronised tokens, appropriate SameSite attributes, validation of the Origin or Referer, and the blocking of unnecessary cross-origin schemes.

CSRF protection obviously does not replace file validation. APIs authenticated by an explicit bearer token also present a different threat model. The principle is simply that an upload, replacement or deletion is a stateful action, and that an attacker must not be able to force a logged-in user’s browser to submit a file without the user’s intentional action.

Isolate file processing in a sandbox

Components that parse files must be isolated from the main application because they execute complex libraries on bytes controlled by an attacker. ImageMagick, Ghostscript, ExifTool, LibreOffice, FFmpeg, OCR engines and PDF libraries are common examples.

A more secure architecture places new files in a quarantine zone and then delegates parsing or conversion to a dedicated worker. This worker receives only the object it needs to process, runs under a non-privileged identity, has strict CPU and memory limits, does not contain any unnecessary secrets, and can access only a minimal filesystem.

Outgoing network connections must be restricted, as a parser or renderer with unrestricted network access could turn active content into an SSRF or an exfiltration channel. Once processing is complete, only the validated or reconstructed result is moved to permanent storage.

Containers, dedicated workers, virtual machines or serverless functions can provide this isolation depending on the environment. However, the sandbox does not replace patch management: it reduces the blast radius of a vulnerability, but the component continues to handle untrusted content and must remain up to date.

Content Disarm and Reconstruction (CDR)

Content Disarm and Reconstruction takes a different approach to antivirus software. Rather than attempting to determine whether each object contained within a document is malicious, the engine parses the file, removes any features not authorised by the security policy, and then reconstructs a new version containing only the necessary elements.

For a PDF, this may involve removing scripts, launch actions, embedded attachments or other active objects before reconstructing the document. For an Office file, the process may remove macros, embedded executables and unsupported objects whilst preserving the text, images and formatting that are useful for business purposes.

The exact policy depends on the format and the expected level of fidelity. CDR is particularly relevant when an organisation regularly receives documents from third parties and requires greater assurance than a simple signature check or virus scan.

The CDR engine itself must be isolated, as it parses untrusted content. Reconstruction reduces the attack surface of the final file but does not make the processing engine free from vulnerabilities.

Logging, monitoring and alerting

Security does not end once a file has been accepted. The logs must make it possible to determine who authorised the upload, what checks were carried out, which storage ID was generated, what processing took place, who subsequently accessed the content, and why a file was rejected or placed in quarantine.

{
  "user_id": "12345",
  "file_id": "7c8f9a4e",
  "original_filename": "invoice.php.jpg",
  "detected_mime": "image/jpeg",
  "upload_result": "rejected",
  "reason": "extension_policy"
}

Notable warning signs include repeated validation failures, attempts to upload executable or configuration extensions, unusual inconsistencies between MIME types and signatures, abnormal volumes, quota exhaustion, an increase in processing errors, antivirus detections, unexpected expansion of archives, attempts at cross-tenant access, and the appearance of atypical formats for a given functionality.

Alerts must be context-sensitive to prevent normal business activity from masking genuinely suspicious behaviour. Event retention must also facilitate investigation: if a malicious file is discovered several days later, the team must be able to trace the original account, the objects concerned, any derived files, the workers who handled the content, and the users who viewed or downloaded it.

Conclusion

Securing a file upload is difficult precisely because a file is never ‘validated once and for all’. The same object may be named in the browser, inspected by application code, analysed by several libraries, processed by workers, stored on a file system or in a bucket, cached by a CDN and finally displayed or opened by another user.

At each of these stages, the way in which the bytes are interpreted may change. Data considered to be an image by an initial check may become an active document for a browser. A PDF accepted by the application may become a malicious input for a conversion engine. A correctly identified archive may produce an arbitrary path after extraction. A storage key deemed safe may allow an object belonging to another workflow to be overwritten.

This is why the most robust security model is based on defence in depth. Formats must be limited to actual requirements, names standardised, content validated on the server side, storage identifiers generated, untrusted files isolated, authorisations applied at the level of each object, content kept inaccessible until all checks are complete, the privileges and resources of parsers restricted, and control maintained over how the final file is rendered.

No single control provides equivalent coverage. An authorised extension does not prove the actual format. A correct MIME type can be forged. Valid magic bytes do not guarantee the security of the entire structure. An antivirus programme may miss an unknown payload. A correctly validated file can still become dangerous if served with incorrect headers or consumed by a vulnerable component.

For a penetration tester, this same logic becomes a methodology. The aim is to map the entire lifecycle, to provoke controlled discrepancies between the various validation layers, to track the file to all secondary consumers, and to test access controls with the same attention as the content itself.

The most critical upload vulnerabilities therefore do not necessarily appear at the time of the POST request. They often arise later, when another component trusts what the upload chain has allowed through. Understanding this time lag between validation, storage, processing and usage is key to auditing and securing these features in the long term.

References

The following resources supplement the principles, testing techniques and security measures presented in this guide:

OWASP File Upload Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html

OWASP Input Validation Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html

PortSwigger Web Security Academy – File upload vulnerabilities: https://portswigger.net/web-security/file-upload

Amazon S3 – Download and upload objects with presigned URLs: https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html

MDN – SVG as an image: https://developer.mozilla.org/en-US/docs/Web/SVG/Guides/SVG_as_an_image

MDN – X-Content-Type-Options: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Content-Type-Options