CWE-434: Unrestricted File Upload
Overview
Unrestricted file upload vulnerabilities occur when applications accept file uploads without properly validating file type, content, size, or destination. Attackers exploit this to upload malicious files including web shells (PHP, JSP, ASPX), executable malware, HTML files with XSS payloads, SVG files with embedded JavaScript, or oversized files causing denial of service. The vulnerability is particularly dangerous when uploaded files are stored within the webroot and can be directly accessed and executed by the web server.
OWASP Classification
A06:2025 - Insecure Design
Risk
Critical: Can lead to remote code execution, complete server compromise, malware distribution, stored XSS, denial of service, and defacement. Unrestricted uploads combined with path traversal can overwrite critical system files. Even "safe" file types like images can contain malicious metadata or be used for social engineering attacks.
Remediation Steps
Core principle: Validate file type through content inspection (not just extension), store uploads outside webroot, rename files, and never execute or serve uploaded files in their original format without sanitization.
Validate File Type by Content
# VULNERABLE - trusts file extension
if filename.endswith('.jpg'):
save_file(filename, data)
# SECURE - validate actual file content
import magic
ALLOWED_TYPES = {'image/jpeg', 'image/png', 'image/gif'}
mime = magic.from_buffer(file_data, mime=True)
if mime not in ALLOWED_TYPES:
return error('Invalid file type')
Store Files Outside Webroot
# VULNERABLE - files in webroot can be executed
UPLOAD_DIR = '/var/www/html/uploads' # Dangerous!
# SECURE - files outside webroot
UPLOAD_DIR = '/var/app_data/uploads' # Not web-accessible
# Serve through application with access controls
@app.route('/file/<file_id>')
def serve_file(file_id):
# Authenticate user, check permissions
if not user.can_access(file_id):
abort(403)
filepath = get_secure_path(file_id)
return send_file(filepath)
Rename Uploaded Files
# VULNERABLE - uses original filename
save_path = os.path.join(UPLOAD_DIR, uploaded_filename)
# SECURE - generate random filename
import uuid
extension = get_validated_extension(uploaded_filename)
new_filename = f"{uuid.uuid4()}.{extension}"
save_path = os.path.join(UPLOAD_DIR, new_filename)
# Store mapping in database
db.store_file(user_id, file_id=new_filename, original_name=uploaded_filename)
Implement File Size Limits
# Limit upload size
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB
if len(file_data) > MAX_FILE_SIZE:
return error('File too large')
Sanitize Image Uploads
from PIL import Image
import io
# Strip metadata and re-encode
image = Image.open(io.BytesIO(file_data))
clean_image = io.BytesIO()
image.save(clean_image, format='PNG') # Re-encode as PNG
Configure Web Server
# Nginx - never execute files in upload directory
location /uploads/ {
# Serve files as downloads only
add_header Content-Disposition "attachment";
add_header X-Content-Type-Options "nosniff";
# Disable script execution
location ~ \.(php|jsp|asp|aspx|cgi)$ {
deny all;
}
}
Scan Uploads for Malware
Integrate anti-virus scanning:
- ClamAV for open-source scanning
- Cloud services: VirusTotal API, AWS GuardDuty, Azure Defender
- Quarantine files until scan completes
Dynamic Scan Guidance
For guidance on remediating this CWE when detected by dynamic (DAST) scanners:
- Dynamic Scan Guidance - Analyzing DAST findings and mapping to source code