Secure File Uploads with Spring Boot
File upload endpoints accept attacker-controlled bytes, filenames, sizes, and content types. A secure design limits requests early, generates server-side names, stores files outside the web root, validates paths, and serves downloads with controlled headers.
Configure request limits
Set limits appropriate for the application:
spring:
servlet:
multipart:
max-file-size: 20MB
max-request-size: 25MB
Also configure limits at the reverse proxy and load balancer. Proxy and application limits should agree so oversized requests are rejected predictably.
Store uploads with generated names
@Service
public class FileStorageService {
private final Path root = Path.of("/srv/myapp/uploads")
.toAbsolutePath()
.normalize();
public FileStorageService() throws IOException {
Files.createDirectories(root);
}
public String store(MultipartFile file) throws IOException {
if (file.isEmpty()) {
throw new IllegalArgumentException("Empty file");
}
String original = Optional.ofNullable(file.getOriginalFilename())
.orElse("upload");
String extension = safeExtension(original);
String storedName = UUID.randomUUID() + extension;
Path destination = root.resolve(storedName).normalize();
if (!destination.getParent().equals(root)) {
throw new SecurityException("Invalid destination");
}
try (InputStream input = file.getInputStream()) {
Files.copy(input, destination, StandardCopyOption.COPY_ATTRIBUTES);
}
return storedName;
}
private String safeExtension(String filename) {
String base = Path.of(filename).getFileName().toString();
int dot = base.lastIndexOf('.');
if (dot < 0) return "";
String extension = base.substring(dot).toLowerCase(Locale.ROOT);
return Set.of(".jpg", ".png", ".pdf").contains(extension)
? extension
: "";
}
}
Never use the client filename as the storage path. Persist the original display name separately after sanitizing control characters.
Validate actual content
The multipart Content-Type and extension are user-supplied. For sensitive workflows, inspect file signatures with a maintained library, decode images before accepting them, and scan untrusted documents with malware tooling. Reject archives unless the product explicitly needs them; archive extraction introduces zip-slip paths and decompression bombs.
Serve files safely
Resolve only server-generated identifiers and verify the normalized result remains under the storage root. For downloads, set a controlled content type and use Content-Disposition: attachment for formats that should not execute in the browser.
@GetMapping("/files/{id}")
public ResponseEntity<Resource> download(@PathVariable String id) throws IOException {
if (!id.matches("[0-9a-fA-F-]{36}\\.(jpg|png|pdf)")) {
return ResponseEntity.badRequest().build();
}
Path file = root.resolve(id).normalize();
if (!file.getParent().equals(root) || !Files.isRegularFile(file)) {
return ResponseEntity.notFound().build();
}
Resource resource = new FileSystemResource(file);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + id + "\"")
.body(resource);
}
Authorization must be checked before returning a resource. Do not rely on an unguessable URL as access control.
Operational safeguards
- Store uploads on a separate volume with no execute permission where practical.
- Apply per-user quotas and rate limits.
- Record ownership, checksum, size, and upload time in the database.
- Clean up abandoned temporary files and failed database transactions.
- Back up metadata and objects consistently.
- Use object storage and presigned uploads for large-scale systems.
- Test filename encoding, concurrent uploads, disk-full behavior, and interrupted requests.
A correct upload controller is only one layer. Reverse-proxy limits, storage permissions, authentication, antivirus policy, observability, and retention rules complete the security model.