Unrestricted upload of dangerous file types

Restrict uploaded file types, storage and processing

Description

If an application does not adequately restrict upload formats and subsequent storage or processing, uploaded files can be executed or interpreted as dangerous content on a server or client.

Potential impact

An uploaded file can execute code if a server or application treats it as a script, such as JSP, ASP or PHP. An interpreter may process the file even when the filesystem does not grant that file an executable permission bit.

Remediation

  1. Do not trust the original filename or client-provided MIME type. Generate names on the server and validate size and actual content format.
  2. Store uploads outside the web root with restricted write permissions. Configure the web server and later processing so uploads are not executed as server code. Directory search permissions are different from file execution permissions.
  3. Compare normalized destination path components with the upload root and atomically create new files only.
  4. Use malware scanning and isolated processing where uploads require further handling.

Examples

Before

java
@PostMapping("/upload")
public String handleFileUpload(@RequestParam("file") MultipartFile file) throws IOException {
    // Save using the client-supplied filename
    String fileName = file.getOriginalFilename();
    File destinationFile = new File("/uploads/" + fileName);
    file.transferTo(destinationFile);
    return "uploadSuccess";
}

After

This Java 11 or later excerpt stores only UTF-8 text. Only trusted administrators may modify the upload root and its parent paths. Also limit multipart request size in Spring. Text-format validation is not malware detection: do not execute stored content or render it directly as HTML.

java
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.UUID;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
public final class SafeUploadController {
    private static final Path UPLOAD_ROOT =
            Path.of("/var/lib/myapp/uploads").toAbsolutePath().normalize();
    private static final long MAX_TEXT_SIZE = 1024 * 1024;

    @PostMapping("/upload")
    public String handleFileUpload(@RequestParam("file") MultipartFile file) throws IOException {
        if (file.isEmpty() || file.getSize() > MAX_TEXT_SIZE) {
            throw new IOException("Invalid file size");
        }

        byte[] content = file.getBytes();
        String text;
        try {
            text = StandardCharsets.UTF_8.newDecoder()
                    .onMalformedInput(CodingErrorAction.REPORT)
                    .onUnmappableCharacter(CodingErrorAction.REPORT)
                    .decode(ByteBuffer.wrap(content))
                    .toString();
        } catch (CharacterCodingException e) {
            throw new IOException("The upload is not valid UTF-8 text", e);
        }
        if (text.codePoints().anyMatch(c -> c == 0
                || (Character.isISOControl(c) && c != '\n' && c != '\r' && c != '\t'))) {
            throw new IOException("Invalid text content");
        }

        String storedName = UUID.randomUUID() + ".txt";
        Path target = UPLOAD_ROOT.resolve(storedName).normalize();
        if (!target.startsWith(UPLOAD_ROOT)) {
            throw new IOException("Invalid upload destination");
        }

        // Precreate UPLOAD_ROOT outside the web root and prevent execution of uploaded code.
        Files.write(target, content, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
        return "uploadSuccess";
    }
}

The first example saves the client-supplied name, risking unintended paths and execution if later interpreted as a script. The second generates a UUID-based .txt name, limits size, validates the actual UTF-8 bytes and checks the destination before saving a new file with CREATE_NEW.

Related CVEs

References