CWE-434: Unrestricted Upload of File with Dangerous Type
Overview
An unrestricted file upload is one the application accepts without checking its type, content, size, or destination. Attackers use that to upload web shells, executable malware, HTML files carrying XSS payloads, SVG files with embedded JavaScript, or oversized files that cause denial of service. The exposure is worst when uploads land inside the webroot, where the web server can serve them back directly.
Which of those outcomes is reachable depends on the stack, and the answer is not the same across the pages below. Whether an uploaded file is executed is a property of the deployment: PHP behind PHP-FPM or mod_php executes .php anywhere it is configured to, a servlet container may compile .jsp, and ASP.NET on .NET Framework behind IIS classic runs .aspx and .ashx. ASP.NET Core does not. It has no handler for those extensions, and UseStaticFiles() does not serve them either: with the default options the static file middleware has no content type for .aspx, .ashx or .cshtml and declines the request, so an uploaded web shell under wwwroot returns 404 (measured on .NET 10; it is served as application/octet-stream only if ServeUnknownFileTypes has been turned on). Where nothing executes, the file still reaches a browser with a content type derived from its name, so an uploaded .html or .svg is stored XSS in the application's origin. Work out which of the two you are looking at before writing up a finding; it changes what the fix has to do. The language pages state it per platform.
OWASP Classification
A06:2025 - Insecure Design
Risk
Critical: Where the platform executes uploaded files, this is remote code execution; where it only serves them back, an uploaded .html or .svg is stored XSS in the application's origin and the site becomes a malware distribution point. Oversized files and archive bombs cause denial of service. Unrestricted uploads combined with path traversal can overwrite application or system files when the upload destination is attacker-controlled. Image formats are not exempt: they carry metadata, and decoding one exercises a parser with its own vulnerabilities.
Remediation Steps
Core Principle: Use an allowlist of business-required file types, validate both extension and content, store uploads outside webroot, rename files, and never execute uploaded files. Serve original uploads only as untrusted downloads with authorization and safe response headers.
Validate Extension and Content
// VULNERABLE - trusts the file extension alone
if filename.ends_with('.jpg'):
save_file(filename, data)
// SECURE - validate extension and actual file content together
ALLOWED_TYPES = {
'image/jpeg': ['.jpg', '.jpeg'],
'image/png': ['.png'],
'image/gif': ['.gif'],
}
extension = lowercase(file_extension(filename))
detected_type = detect_mime_from_content(file_data) // inspect bytes, not the client-supplied Content-Type header
if detected_type not in ALLOWED_TYPES:
return error('Invalid file type')
if extension not in ALLOWED_TYPES[detected_type]:
return error('File extension does not match content')
File signatures and MIME detection are useful checks, but they are not a complete security boundary. Keep the allowlist narrow, reject ambiguous or polyglot files where possible, and parse or re-encode files with maintained libraries before trusting them as images, documents, or archives.
Store Files Outside Webroot
// VULNERABLE - files in webroot can be requested and executed directly
UPLOAD_DIR = '/var/www/html/uploads'
// SECURE - files outside webroot, served only through the application
UPLOAD_DIR = '/var/app_data/uploads'
function serve_file(file_id):
if not current_user.can_access(file_id):
return 403
filepath = lookup_secure_path(file_id)
return send_as_attachment(filepath, download_name = original_display_name(file_id), content_type = 'application/octet-stream')
Rename Uploaded Files
// VULNERABLE - uses the original filename as the storage path
save_path = join_path(UPLOAD_DIR, uploaded_filename)
// SECURE - random storage name, original name kept only for display
extension = ALLOWED_TYPES[detected_type] // from the sniffed content, not the filename
display_name = sanitize_display_name(uploaded_filename)
new_filename = generate_uuid() + extension
save_path = join_path(UPLOAD_DIR, new_filename)
db.store_file(user_id, file_id = new_filename, original_name = display_name)
Do not use the original filename in the storage path, and take the stored extension from the type you detected rather than from the name. A UUID plus a client-chosen suffix still lets the attacker pick the half of the name that decides how the file is served or executed - a real PNG uploaded as evil.php becomes <uuid>.php, which passes every content check and is stored under the extension the check just refused to accept. Normalize the display name separately, remove path separators and control characters, and enforce a maximum length before showing it back to users.
Implement File Size Limits
MAX_FILE_SIZE = 5 * 1024 * 1024 // 5 MB
// reject before reading the whole request body into memory
if request_content_length() > MAX_FILE_SIZE:
return error('File too large')
Apply limits at the reverse proxy, application server, and application layer. For archives and compressed formats, also limit decompressed size, file count, nesting depth, and extraction paths to avoid archive bombs and path traversal.
Sanitize Image Uploads
// strip metadata and re-encode through a maintained image library
set_decompression_bomb_limit(20_000_000) // pixels
image = decode_image(file_data)
image.verify()
image = decode_image(file_data) // re-decode after verify, per the library's own guidance
image = resize_within(image, max_width = 4096, max_height = 4096)
clean_bytes = encode_image(image, format = 'PNG') // re-encoding drops embedded scripts/metadata the original format could carry
Re-encoding reduces metadata and active-content risk for raster images, but it does not replace patching the image library. Reject formats that can contain scriptable content, such as SVG, unless the application has a dedicated sanitizer and a safe serving policy.
Configure Web Server
Storing uploads outside webroot is the preferred control. If uploaded files must be reachable through the web server, configure the upload location so it cannot execute scripts and serves untrusted content as downloads. Force Content-Disposition: attachment and X-Content-Type-Options: nosniff on responses from that location, and deny any request matching a server-side-executable extension within it. Which extensions those are is a property of your stack rather than a fixed list: .php matters behind PHP-FPM or mod_php, .jsp behind a servlet container, .cgi where CGI is enabled, and .aspx/.ashx behind IIS classic but not on ASP.NET Core, which has no handler for them - blocking them there stops nothing while reading like protection. This is a denylist and it is deliberately the second line, behind the allowlist above and behind storing uploads outside webroot; it is not a substitute for either.
Scan Uploads for Malware
Scan uploads before they become reachable:
- ClamAV for open-source scanning
- Hosted scanners such as the VirusTotal API, AWS GuardDuty, or Azure Defender
- Quarantine the file until the scan finishes
Malware scanning is defense-in-depth, not a substitute for type allowlisting and safe storage. Do not submit sensitive user files to third-party scanning services unless the data-sharing and retention implications are acceptable for the application.
Protect Upload Endpoints
Treat upload routes as state-changing operations:
- Require authentication and authorization before accepting the file
- Protect browser-based upload forms from CSRF
- Rate-limit upload attempts and enforce per-user storage quotas
- Log upload decisions with safe metadata only, not raw file contents
Language-Specific Guidance
- C# - ASP.NET Core IFormFile, file signature validation, what actually executes on Kestrel
- Go - net/http multipart uploads, http.DetectContentType, safe storage paths
- Java - Spring MultipartFile, Apache Tika content detection, storage outside webroot
- JavaScript - Express/multer fileFilter and storage engine, file-type magic-byte checks
- PHP - $_FILES and move_uploaded_file(), finfo MIME detection, disabling script execution
- Python - Django/Flask/FastAPI upload handling, python-magic, filename sanitization