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

30 min

Lesson 1 of 2

1d streak

The 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:

src/main/resources/application.yml
spring:
  servlet:
    multipart:
      enabled: true
      max-file-size: 10MB
      max-request-size: 25MB
      file-size-threshold: 1MB
      location: /tmp/uploads

file-size-threshold Memory Safety

Files smaller than 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

The Content-Type header is a user-controlled HTTP header supplied by the browser or attacker curl command. Always inspect the raw file magic bytes (file signature) using Apache Tika to determine the authentic MIME type.
src/main/java/com/example/upload/FileValidationService.java
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:

snippet.java
// 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.

FileUploadValidationLab.java
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;
    }
}
Streaming via file.getInputStream() prevents OutOfMemoryError by avoiding large in-memory byte arrays.
StringUtils.cleanPath() and .. checks defend against directory traversal attacks.
Always validate magic bytes with Apache Tika rather than relying solely on client Content-Type headers.

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

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

Supplementary Module Resources