Intermediate · Build
Multipart Uploads & File Validation
Accept files through multipart/form-data with explicit limits and content checks—never trusting names, types, or sizes the client claims.
Primary source: Spring Boot 3.5 Reference: Uploading Files
Lesson 1 of 2
1d streakThe Architectural Question
Naive upload handlers exhaust heap on large files, store executables renamed to .pdf, and let path-traversal filenames escape the upload directory.
What this guide proves
- Bind MultipartFile in a controller with size limits configured
- Validate real media type and sanitize stored filenames
- Stream uploads so memory does not scale with file size
Multipart File Upload Configuration & Memory Thresholds
In Spring Boot, file uploads are handled via the Servlet multipart parser. Unrestricted file upload configurations are a major vector for Denial of Service (DoS) and OutOfMemoryError attacks when malicious clients stream multi-gigabyte payloads to an unconstrained server.
Configure explicit multipart limits in application.yml to reject oversized uploads before they consume memory or disk space:
spring:
servlet:
multipart:
enabled: true
max-file-size: 10MB
max-request-size: 25MB
file-size-threshold: 1MB
location: /tmp/uploadsfile-size-threshold Memory Safety
file-size-threshold (e.g. 1MB) are kept in memory for high-speed processing. Files exceeding this threshold are written directly to temporary disk storage (location), preventing large uploads from saturating the JVM heap.Security: Magic-Byte Content-Type Validation vs Client Headers
Never trust file.getContentType() or the file extension from file.getOriginalFilename(). Attackers easily bypass extension checks by renaming executable scripts (e.g. exploit.php.png or malicious.sh with Content-Type: image/png).
The Content-Type Spoofing Vulnerability
package com.example.upload;
import org.apache.tika.Tika;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.Set;
@Service
public class FileValidationService {
private final Tika tika = new Tika();
private static final Set<String> ALLOWED_TYPES = Set.of(
"image/jpeg",
"image/png",
"application/pdf"
);
public void validate(MultipartFile file) {
if (file.isEmpty()) {
throw new IllegalArgumentException("Cannot upload empty file");
}
try (InputStream is = file.getInputStream()) {
// Tika inspects the initial magic bytes of the file stream directly
String detectedType = tika.detect(is);
if (!ALLOWED_TYPES.contains(detectedType)) {
throw new IllegalArgumentException("Forbidden file type detected: " + detectedType);
}
} catch (Exception ex) {
throw new RuntimeException("Failed to inspect file magic bytes", ex);
}
}
}Streaming Large Files Without In-Memory Buffering
Calling file.getBytes() allocates the entire file into a contiguous Java byte array on the heap. If 50 users simultaneously upload 20MB PDF documents, file.getBytes() allocates 1GB of heap memory in seconds, triggering severe Garbage Collection pauses.
Use it when
Always process uploads via file.getInputStream() and stream bytes directly to disk or cloud object storage using buffered channels.
Reconsider when
Never call file.getBytes() for any file larger than a few kilobytes.
Path Traversal Defense and Upload Proving Checkpoints
Sanitize file names to prevent path traversal attacks where an attacker uploads a file named ../../../../etc/cron.d/exploit:
// Clean filename of relative directory traversal sequences
String originalName = file.getOriginalFilename();
String cleanFileName = StringUtils.cleanPath(Objects.requireNonNull(originalName));
if (cleanFileName.contains("..")) {
throw new SecurityException("Filename contains invalid path sequence: " + cleanFileName);
}Active Recall Checkpoint
Why is file.getContentType() insufficient for verifying uploaded file safety?
Hint: Differentiate client-supplied metadata from actual binary content.
Show answer & explanation
file.getContentType() is parsed directly from the incoming HTTP request header sent by the client. Any attacker can send arbitrary binary executables while claiming Content-Type: image/jpeg.
Working Implementation
Code Walkthrough
Add POST /api/tasks/{id}/attachments accepting PDFs and images up to 5 MB and reject everything else with clear errors.
package com.example.file;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Objects;
import java.util.Set;
@Service
public class FileUploadValidationLab {
private static final Set<String> ALLOWED_EXTENSIONS = Set.of("png", "jpg", "jpeg", "pdf");
public Path storeFileSafely(MultipartFile file, Path uploadDir) throws IOException {
if (file.isEmpty()) {
throw new IllegalArgumentException("Cannot upload empty file");
}
String rawName = Objects.requireNonNull(file.getOriginalFilename());
String cleanName = StringUtils.cleanPath(rawName);
// Path traversal defense
if (cleanName.contains("..")) {
throw new SecurityException("Path traversal attempt detected: " + cleanName);
}
// Extension validation
String ext = StringUtils.getFilenameExtension(cleanName);
if (ext == null || !ALLOWED_EXTENSIONS.contains(ext.toLowerCase())) {
throw new IllegalArgumentException("Unsupported extension: " + ext);
}
Path target = uploadDir.resolve(cleanName);
// Stream directly to disk: NEVER call file.getBytes() to prevent JVM heap exhaustion!
try (InputStream is = file.getInputStream()) {
Files.copy(is, target, StandardCopyOption.REPLACE_EXISTING);
}
return target;
}
}Synthesis & Production Verification
Production Acceptance Criteria
- Reject oversized uploads with 413 and wrong types with 415
- Store files under generated names with no user-controlled path segments
- Prove memory stays flat across upload sizes
Key Takeaway
Secure multipart file uploads enforce strict memory thresholds, streaming I/O without in-memory buffering, and Apache Tika magic-byte inspection to prevent remote code execution.
Architectural & Technical Review
Core Interview Questions
Q01Why is checking file extension or file.getContentType() insufficient for verifying uploaded file safety?Show answer
The Content-Type header and original file name are client-controlled values. An attacker can upload a malicious script disguised with a .png extension and fake MIME header. Always inspect raw magic bytes using Apache Tika.
Q02How does configuring spring.servlet.multipart.file-size-threshold protect JVM heap memory?Show answer
Files smaller than the threshold remain in memory for fast access, while files exceeding it are written directly to temporary disk storage, preventing multi-megabyte uploads from exhausting heap memory.
Q03Why should file.getBytes() never be called on user-uploaded files in production?Show answer
file.getBytes() allocates the entire file into a contiguous Java byte array in the JVM heap. Concurrent uploads of large files trigger massive heap churn and frequent OutOfMemoryError crashes. Always stream via file.getInputStream().
Production Pitfalls
Critical Anti-Patterns
- Pitfall: Calling file.getBytes() on uploaded files instead of streaming via InputStream — Buffers multi-megabyte files directly in JVM heap memory, causing rapid heap exhaustion under concurrent uploads.
- Pitfall: Failing to sanitize file.getOriginalFilename() against path traversal sequences (../) — Allows malicious users to write uploaded files to arbitrary filesystem paths like /etc/cron.d/.
Primary Documentation & Specifications
Lesson Primary References
Supplementary Module Resources