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:

1
2
3
4
5
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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.