Application Security RCE

File Uploads in Practice: How an Innocent Image Becomes Remote Code Execution

Author
Dulanjana Fernando
Aug 02, 2026  •  9 min read  •  77 views
File Uploads in Practice: How an Innocent Image Becomes Remote Code Execution

File upload features such as profile pictures, document submissions and image galleries are common in modern web applications. They also carry a dangerous threat to the application and server. When the application fully trusts and accepts the file uploads from a user and writes them to a directory, a single misstep can allow an attacker to execute code on the server infrastructure. This article discusses how an attacker can bypass frontend MIME type and extension checks, how polygot files trick image parsers and how to mitigate the file upload vulnerability by isolating the file upload pipeline.

1. "Images Only" is not a Security Control

File upload is one of the most commonly available and simple features of a modern web application. To implement an image file upload, we use accept="image/png, image/jpeg" to restrict the user from selecting a file type other than an image file. This feels like a security control because the user can not select a different file type to upload.
However, this is purely a User Experience(UX) feature, not a security control. An attacker can intercept the file upload and change the file binary payload or craft and send a raw binary payload with a mallicious files using tools like Postman, BurpSuit or a custom Python script.

2. Bypassing Client and Server Side Validation

As mentioned above, accept="....." is not a security boundary. Attackers can easily bypass the client-side validation by intercepting the file upload request using tools like BurpSuit or crafting a malicious request using cURL, Postman or a custom Python script.

Most common server-side validation relies on metadata supplied from the client-side, along with the file. Attackers use three main ways to bypass these server-side validations.

MIME-Type Tampering

When a user uploads a file, the browser sends a multipart/form-data HTTP request that contains the content type of the file.

HTTP Request and Vulberable Code Implimentation
# HTML Request
POST /api/upload HTTP/1.1
Host: app.example.com
Content-Type: multipart/form-data; boundary=---------------------------123456

-----------------------------123456
Content-Disposition: form-data; name="avatar"; filename="payload.php"
Content-Type: application/x-php


-----------------------------123456--


# Vulnerable Server-side validation code
if ($_FILES['avatar']['type'] !== 'image/png') {
    die("Only PNG images allowed!");
}

When the server trusts the user and reads the uploaded file type directly from the HTTP request, the attacker can use this to bypass this validation by intercepting the file upload request using BurpSuit and changing the content-type header to Content-Type: image/png. This will result in a successful file upload even though the file is not an image file.

Extension Manipulation & Blacklist Bypasses

To overcome this, developers sometimes use extension blacklists to block files with certain file extensions from being uploaded. But, in some cases, for example, if the Apache server is configured with AddHandler application/x-httpd-php .php, it can also execute .phtml, .phar files as PHP files.

Furthermore, attackers can use double extensions, case sensitivity, trailing characters or null byte between extensions to bypass these extensions.

Extension Bypass Methods
Alternative Extensions: .phtml, .php5, .php7, .phar, .pht, .inc  (instead of .php)
Case Sensitivity:       avatar.Php, avatar.PHP
Double Extensions:      avatar.png.php or avatar.php.png
Trailing Characters:    avatar.php. or avatar.php%20
Null Byte Injection:    avatar.php%00.png or avatar.php\0.png

Polyglot Files

As a slightly more invasive validation, web applications might implement file content inspection. Most built-in functions like getimagesize() or exif_imagetype() read the file's Magic Bytes (header signature at the beginning of the file) to validate the file type.
To bypass this validation, the attacker can craft a polygot file by inserting the magic bytes into the beginning of the file to trick the server into believing the PHP file is an image file.

3. Code Breakdown - Vulnerable vs. Hardened

A secure upload handler should always treat the uploaded file as malicious until it's proven it's not. This can be done using extension whitelisting, stripping the original file names and re-encoding the file to destroy polygots and embedded payloads.

# Vulnerable Code Implementation
$uploadDir = '/var/www/html/public/uploads/';

$fileName = $_FILES['avatar']['name']; // Uses raw client-supplied filename
$fileType = $_FILES['avatar']['type']; // Trusts client-supplied MIME header

// Checks client-supplied MIME type
if ($fileType === 'image/png' || $fileType === 'image/jpeg') {
    // Moves file into a publicly accessible web folder, allowing the user to execute the file as necessary
    $destination = $uploadDir . $fileName; 
    
    if (move_uploaded_file($_FILES['avatar']['tmp_name'], $destination)) {
        echo "File uploaded to /uploads/" . $fileName;
    }
}

The above vulnerable code have following issues.

  • Trusts the user-supplied Content-Type, which allows the attacker to bypass the validation by changing the content-type in their payload.
  • Preserves the client-supplied file name, which can easily be used to execute the payload by the attacker.
  • Retains the original file structure, allowing polyglot file uploads.
  • Writes directly into the web root that is configured to execute static scripts.
# Hardened Code Implementation
$uploadDir = '/var/www/storage/uploads/'; // not using web root

$originalName = $_FILES['avatar']['name'] ?? '';
$tempPath = $_FILES['avatar']['tmp_name'] ?? '';

// Strict Extension Whitelist Check (this helps to block alternative extensions)
$allowedExtensions = ['png', 'jpg', 'jpeg'];
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));

if (!in_array($extension, $allowedExtensions, true)) {
    http_response_code(400);
    exit(json_encode(['error' => 'Invalid file extension.']));
}

// Discard Client Filename; Generate Cryptographically Secure Name
$randomBytes = bin2hex(random_bytes(16));
$safeFileName = $randomBytes . '.' . $extension;
$destination = $uploadDir . $safeFileName;

// Re-encode Image to Destroy Polyglots & Malicious EXIF Payloads
$rawContent = @file_get_contents($tempPath);
$image = @imagecreatefromstring($rawContent);

if (!$image) {
    http_response_code(400);
    exit(json_encode(['error' => 'Invalid or corrupted image file.']));
}

// Write Re-rendered Image to Non-Web-Root Disk Location
$success = false;
if ($extension === 'png') {
    $success = imagepng($image, $destination);
} else {
    $success = imagejpeg($image, $destination, 85);
}

// Clean up memory resources
imagedestroy($image);

if (!$success) {
    http_response_code(500);
    exit(json_encode(['error' => 'Failed to process file.']));
}

echo json_encode(['status' => 'success', 'file_id' => $safeFileName]);

4. Server-Side Defense

Even if the application layer allowed a malicious file to be uploaded, it does not become a Remote Code Execution(RCE) vulnerability if the server layer is configured properly not to execute the uploaded files.
If the web application must upload and store files on the local storage, the web server should be configured explicitly to block dynamic file processing inside the file upload directories.

# Nginx
# Block script execution in the uploads directory
location ^~ /uploads/ {
    # Ensure PHP-FPM or other upstream handlers never execute scripts here
    location ~ \.(php|php5|phtml|phar|cgi|pl|asp|aspx|jsp)$ {
        deny all;
        return 404;
    }
}

# Apache
<Directory "/var/www/html/public/uploads">
    # Disable CGI script execution
    Options -ExecCGI -Indexes
    
    # Disable the PHP engine inside this folder
    <IfModule mod_php.c>
        php_flag engine off
    </IfModule>
    
    # Force Apache to handle all files as static downloads
    SetHandler default-handler
</Directory>

5. The Modern Architectural Fix - Offloading to Cloud Object Storage

Hardening the application code and server configurations still leaves you with fighting against the rapid changes. One developer's commit can break the file validation, or one server configuration mistake can allow malicious file uploads and execution. As long as the application server is receiving untrusted files and storing them, it carries the unnecessary operational risk.

With the rise of cloud services, the modern architectural fix is to offload the file storage and access to Cloud Object Storage like Google Cloud Storage, Amazon S3 Buckets. This offload not only eliminates the threat but also re-architects the web application with improved performance.

  • Storage buckets are key-value object storage, not operating system file systems. They do not have interpreters or runtimes, this eleminates the surface for the malicious script to execute on.
  • Uploaded files are served from a separate and dedicated storage domain that does not belong to the application. This removes the ability to access application cookies or session tokens.
  • Slow uploads, massive files, do not slow the application server, as the file is served from a different domain.

6. Final Thoughts: Treat Files as Untrusted Code

File upload, by definition, is a mechanism to allow users to write untrusted data directly into the server infrastructure. If we consider a file upload as simply moving a file from the client's computer to the server's file system, it leaves a door open to Remote Code Execution(RCE).

Whenever possible, removing the file handling from the web application server and offloading it to a cloud storage service ensures the execution environment is removed, strict boundaries are enforced, and high-risk operation is transformed into a safe and scalable workflow.

Tags:
RCE

You might also like...

Top-Down Web Framework Code Review: A Step-by-Step Security Audit Methodology
Application Security Code Review
Top-Down Web Framework Code Review: A Step-by-Step Security Audit Methodology

When auditing a modern web application, jumping straight into reading the contro...

Read More
IDOR in Practice: How Broken Access Control Happens in Real Applications
Application Security IDOR
IDOR in Practice: How Broken Access Control Happens in Real Applications

Insecure Direct Object Reference(IDOR) vulnerability arises when the application...

Read More

Stay Updated

Get notified when new walkthroughs and security articles are published.