What is Unrestricted File Upload?
Unrestricted file upload occurs when an application accepts files from users without properly validating their content, type, or name. At minimum, attackers can upload files that cause denial of service (large files, zip bombs). At worst, they can upload server-side scripts (webshells) and achieve Remote Code Execution (RCE) if the upload directory is web-accessible.
Even when direct execution is prevented, malicious files can introduce stored XSS (e.g., an SVG with embedded script), SSRF via crafted XML in Office documents, or path traversal if the original filename is used without sanitization.
How exploitation works
An attacker uploads a PHP webshell to a profile photo endpoint:
POST /api/upload-avatar HTTP/1.1
Content-Type: multipart/form-data
--boundary
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/jpeg
<?php system($_GET['cmd']); ?>
--boundary--
If the server stores the file under its original name in a web-accessible directory and the web server executes PHP, the attacker visits https://site.com/uploads/shell.php?cmd=id and has achieved RCE.
Vulnerable code examples
C# / ASP.NET Core
// VULNERABLE: Uses original filename, stores in web root, no type validation
[HttpPost("upload")]
public async Task<IActionResult> Upload(IFormFile file)
{
var path = Path.Combine("wwwroot/uploads", file.FileName); // Path traversal risk
using var stream = System.IO.File.Create(path);
await file.CopyToAsync(stream);
return Ok(new { url = "/uploads/" + file.FileName });
}
PHP
// VULNERABLE: Trusts Content-Type header, uses original filename
$target = "uploads/" . $_FILES['file']['name'];
move_uploaded_file($_FILES['file']['tmp_name'], $target);
Secure code examples
C# / ASP.NET Core — validated upload
// SECURE: Extension allow-list, magic bytes check, random filename, outside web root
private static readonly HashSet<string> AllowedExtensions = new() { ".jpg", ".jpeg", ".png", ".gif" };
private static readonly string UploadPath = "/var/app/uploads"; // Outside wwwroot
[HttpPost("upload")]
public async Task<IActionResult> Upload(IFormFile file)
{
var ext = Path.GetExtension(file.FileName).ToLowerInvariant();
if (!AllowedExtensions.Contains(ext)) return BadRequest("File type not allowed.");
if (file.Length > 5 * 1024 * 1024) return BadRequest("File too large.");
// Validate magic bytes for images
using var reader = new BinaryReader(file.OpenReadStream());
var header = reader.ReadBytes(4);
if (!IsValidImageHeader(header)) return BadRequest("Invalid file content.");
var safeFileName = $"{Guid.NewGuid()}{ext}";
var fullPath = Path.Combine(UploadPath, safeFileName);
using var stream = System.IO.File.Create(fullPath);
await file.CopyToAsync(stream);
return Ok(new { id = safeFileName });
}
Python / Django — secure upload
import uuid, os, imghdr
from django.core.exceptions import ValidationError
ALLOWED_TYPES = {'jpeg', 'png', 'gif'}
MAX_SIZE = 5 * 1024 * 1024
def handle_upload(request):
f = request.FILES['file']
if f.size > MAX_SIZE:
raise ValidationError("File too large")
img_type = imghdr.what(f)
if img_type not in ALLOWED_TYPES:
raise ValidationError("File type not permitted")
# Generate a random name — never use original filename
safe_name = f"{uuid.uuid4()}.{img_type}"
save_path = os.path.join('/var/uploads', safe_name) # Outside web root
with open(save_path, 'wb') as dest:
for chunk in f.chunks():
dest.write(chunk)
return safe_name
What Offensive360 detects
- Unvalidated file extensions — Upload handlers that do not check or allow-list file extensions
- Original filename usage — Code that uses
file.FileNameor$_FILES[...]['name']directly as the storage path - Content-Type trust — Validation relying solely on the
Content-Typeheader (attacker-controlled) - Web-accessible upload paths — Files stored within the web server’s document root without execution restrictions
- Missing size limits — Upload handlers with no file size constraint
Remediation guidance
-
Validate by content, not extension or MIME type — Check magic bytes (file header bytes) to confirm the actual file format.
-
Use an extension allow-list — Define exactly which extensions are permitted; deny everything else.
-
Generate random filenames — Never use the original filename. Generate a UUID and append the validated extension.
-
Store uploads outside the web root — Files should not be directly accessible via URL. Serve them through a controlled endpoint that sets
Content-Disposition: attachment. -
Set file size limits — Enforce maximum file sizes at both the application and web server levels.
-
Re-encode images — For image uploads, re-encode the file server-side (e.g., with ImageSharp or Pillow) to strip any embedded malicious content.