File Upload Security: Extension Restrictions and Execution Prevention
File upload functionality is a primary attack vector for uploading web shells or distributing malicious files. Configure a secure upload environment through extension restrictions, file size limits, and execution prevention in the upload directory.
Upload Security Threat Modelβ
Attack scenario:
1. Attacker uploads image.php.jpg or shell.php
2. Server allows PHP execution in the upload directory
3. Attacker accesses https://example.com/uploads/shell.php
4. Server is fully compromised
Nginx Upload Directory Execution Preventionβ
server {
# Upload directory: completely block script execution
location /uploads/ {
alias /var/www/uploads/;
# Block execution of all script files (key setting)
location ~* \.(php|php3|php4|php5|phtml|pl|py|rb|sh|cgi|jsp|asp|aspx)$ {
deny all;
return 403;
}
# Download-only settings
add_header Content-Disposition "attachment"; # Prevent browser execution
add_header X-Content-Type-Options "nosniff"; # Prevent MIME sniffing
# Disable autoindex
autoindex off;
}
}
File Upload Size Limitsβ
# /etc/nginx/nginx.conf β http block or server block
http {
# Global upload size limit (default: 1MB)
client_max_body_size 10m; # 10MB
server {
# General requests
client_max_body_size 1m;
# Increase size limit only for upload endpoints
location /api/upload {
client_max_body_size 50m; # 50MB
proxy_pass http://backend;
proxy_request_buffering off; # Large files: disable buffering
}
}
}
# Apache upload size limit
<Location "/upload">
LimitRequestBody 52428800 # 50MB (bytes)
</Location>
Spring Boot Upload Security Implementationβ
# application.yml
spring:
servlet:
multipart:
max-file-size: 10MB # Maximum size per file
max-request-size: 50MB # Maximum total request size
enabled: true
@Service
public class FileUploadService {
// Allowed extension whitelist
private static final Set<String> ALLOWED_EXTENSIONS = Set.of(
"jpg", "jpeg", "png", "gif", "webp", // Images
"pdf", "doc", "docx", "xls", "xlsx", // Documents
"zip", "tar", "gz" // Archives
);
// Allowed MIME type whitelist
private static final Set<String> ALLOWED_MIME_TYPES = Set.of(
"image/jpeg", "image/png", "image/gif", "image/webp",
"application/pdf",
"application/zip"
);
public String uploadFile(MultipartFile file) {
// 1. Check for empty file
if (file.isEmpty()) {
throw new IllegalArgumentException("File is empty");
}
// 2. File size limit (verify at service level as well)
if (file.getSize() > 10 * 1024 * 1024) { // 10MB
throw new IllegalArgumentException("File size exceeds 10MB");
}
// 3. Extension validation (whitelist)
String originalFilename = file.getOriginalFilename();
String extension = getExtension(originalFilename).toLowerCase();
if (!ALLOWED_EXTENSIONS.contains(extension)) {
throw new IllegalArgumentException("Unsupported file format: " + extension);
}
// 4. MIME type validation (content-based)
String mimeType = detectMimeType(file);
if (!ALLOWED_MIME_TYPES.contains(mimeType)) {
throw new IllegalArgumentException("Unsupported MIME type: " + mimeType);
}
// 5. Randomize filename (never use original filename)
String safeFilename = UUID.randomUUID() + "." + extension;
// 6. Save to upload directory (recommended: outside web root)
Path uploadPath = Paths.get("/var/uploads", safeFilename);
Files.copy(file.getInputStream(), uploadPath, StandardCopyOption.REPLACE_EXISTING);
return safeFilename;
}
private String getExtension(String filename) {
if (filename == null || !filename.contains(".")) {
return "";
}
// Path traversal attack prevention: extract only after the last dot
return filename.substring(filename.lastIndexOf('.') + 1);
}
private String detectMimeType(MultipartFile file) throws IOException {
// Use Apache Tika for content-based MIME type detection (prevents extension spoofing)
// implementation 'org.apache.tika:tika-core:2.x.x'
Tika tika = new Tika();
return tika.detect(file.getInputStream());
}
}
Secure Filename Processingβ
// Path Traversal attack prevention
public String sanitizeFilename(String filename) {
if (filename == null) return null;
// Remove path separators (prevents ../../etc/passwd attacks)
filename = filename.replaceAll("[/\\\\:*?\"<>|]", "_");
// Remove parent directory references
filename = filename.replace("..", "");
// Remove leading/trailing dots and whitespace
filename = filename.strip().replaceAll("^\\.+", "");
// Maximum length limit
if (filename.length() > 100) {
filename = filename.substring(0, 100);
}
return filename.isEmpty() ? "unnamed" : filename;
}
Upload File Storage Locationβ
# Recommended: Store outside web root and serve via Nginx
# Files stored in /var/uploads/ (outside web root)
# Nginx serves internally
server {
# Request /files/{uuid}.jpg β returns /var/uploads/{uuid}.jpg
location /files/ {
# Allow only internal redirects (no direct access)
internal;
alias /var/uploads/;
# Force download
add_header Content-Disposition "attachment";
}
# When API returns X-Accel-Redirect header, Nginx sends the file
location /api/download/ {
proxy_pass http://backend;
# Backend: response.setHeader("X-Accel-Redirect", "/files/" + filename);
}
}
Image File Reprocessing (Image Stripping)β
// Remove malicious code hidden in image files (using ImageMagick)
// Re-encode uploaded images to remove metadata/scripts
public void sanitizeImage(Path inputPath, Path outputPath) throws Exception {
ProcessBuilder pb = new ProcessBuilder(
"convert",
inputPath.toString(),
"-strip", // Remove metadata (EXIF, IPTC, etc.)
"-auto-orient", // Rotate to correct orientation
outputPath.toString()
);
Process process = pb.start();
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new RuntimeException("Image processing failed");
}
}
Virus Scan Integration (ClamAV)β
# Install ClamAV
sudo apt install clamav clamav-daemon
sudo freshclam # Update virus database
sudo systemctl start clamav-daemon
// ClamAV integration in Java
public boolean scanFile(Path filePath) throws IOException {
ProcessBuilder pb = new ProcessBuilder(
"clamdscan", "--no-summary", filePath.toString()
);
Process process = pb.start();
int exitCode = process.waitFor();
// 0: clean, 1: virus found, 2: error
if (exitCode == 1) {
Files.delete(filePath); // Immediately delete virus-infected file
throw new SecurityException("Virus detected in file");
}
return exitCode == 0;
}