Modern web frameworks provide built-in file upload handling, but misconfiguration and misuse of these features can still lead to vulnerabilities. In this lesson, we will examine how file uploads are handled in popular frameworks and identify common misconfigurations that penetration testers should look for.
Multer is the most popular file upload middleware for Express.js. It handles multipart/form-data requests and provides options for file filtering, size limits, and storage configuration.
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
// VULNERABLE: No file filter — accepts any file
const storage = multer.diskStorage({
destination: './uploads/',
filename: (req, file, cb) => {
// VULNERABLE: Preserves original extension
cb(null, Date.now() + path.originalname);
}
});
const upload = multer({ storage: storage });
app.post('/upload', upload.single('file'), (req, res) => {
res.send('File uploaded to: /uploads/' + req.file.filename);
});The vulnerable code above has no file filter, preserves the original filename (including extension), and stores files in a web-accessible directory. An attacker can upload a .js file containing malicious server-side code if the server is misconfigured, or an .html file for XSS.
// SECURE: Proper file filter and random filename
const fileFilter = (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type'), false);
}
};
const secureStorage = multer.diskStorage({
destination: './uploads/',
filename: (req, file, cb) => {
// SECURE: Random filename with validated extension
const ext = path.extname(file.originalname).toLowerCase();
cb(null, `${Date.now()}-${Math.random().toString(36).substring(2)}${ext}`);
}
});
const upload = multer({
storage: secureStorage,
fileFilter: fileFilter,
limits: { fileSize: 5 * 1024 * 1024 } // 5MB limit
});Django provides a file upload system through its FileField and ImageField model fields. By default, Django stores uploaded files in the MEDIA_ROOT directory and serves them from MEDIA_URL.
# models.py
from django.db import models
class UserProfile(models.Model):
# VULNERABLE: No validation on the file field
avatar = models.FileField(upload_to='avatars/')
# SECURE: Using a custom validator
import os
from django.core.exceptions import ValidationError
def validate_file_extension(value):
ext = os.path.splitext(value.name)[1].lower()
valid_extensions = ['.jpg', '.jpeg', '.png', '.gif']
if ext not in valid_extensions:
raise ValidationError('Unsupported file extension.')
def validate_file_type(value):
import magic
file_type = magic.from_buffer(value.read(1024), mime=True)
if file_type not in ['image/jpeg', 'image/png', 'image/gif']:
raise ValidationError('Invalid file type.')
class SecureProfile(models.Model):
avatar = models.FileField(
upload_to='avatars/',
validators=[validate_file_extension, validate_file_type]
)Spring Boot handles file uploads through the MultipartFile interface. Common vulnerabilities include trusting the original filename and not validating content.
@PostMapping("/upload")
public String handleUpload(@RequestParam("file") MultipartFile file) {
// VULNERABLE: Using original filename directly
String filename = file.getOriginalFilename();
Path path = Paths.get("/uploads/" + filename);
file.transferTo(path);
return "Uploaded: " + filename;
}
// SECURE: Validate and sanitize
@PostMapping("/upload")
public String handleSecureUpload(@RequestParam("file") MultipartFile file) {
String originalName = file.getOriginalFilename();
String extension = originalName != null ?
originalName.substring(originalName.lastIndexOf(".")).toLowerCase() : "";
List<String> allowed = Arrays.asList(".jpg", ".jpeg", ".png", ".gif");
if (!allowed.contains(extension)) {
throw new RuntimeException("Invalid file type");
}
// Generate random filename
String safeName = UUID.randomUUID().toString() + extension;
Path path = Paths.get("/var/app/uploads/", safeName);
file.transferTo(path);
return "Uploaded: " + safeName;
}| Misconfiguration | Framework | Impact |
|---|---|---|
| Serving uploads from web root | All | Direct execution of uploaded scripts |
| Trusting original filename | All | Path traversal, extension spoofing |
| No file size limits | All | DoS via large uploads |
| Storing uploads with execute permissions | All | Code execution |
| Using development server in production | Django, Flask | Debug info leakage, arbitrary code execution |
| Missing CSRF protection on upload endpoint | All | Unauthenticated uploads |
| Serving uploads with wrong Content-Type | All | XSS via HTML/SVG uploads |
💡 When testing file uploads in modern frameworks, check the framework's default behavior. Many frameworks have secure defaults that developers override — for example, Django's FileSystemStorage is relatively secure by default, but developers often customize it in ways that introduce vulnerabilities.
Many modern applications upload files directly to cloud storage services like AWS S3 or Google Cloud Storage. While this offloads the security responsibility to the cloud provider, misconfigurations in bucket policies, pre-signed URLs, and CORS settings can still lead to vulnerabilities.
Verify exercises to earn ★ 150 XP and unlock next lab level.