Skip to content

CWE-377: Insecure Temporary File - Java

Overview

Insecure temporary file creation in Java occurs when applications create files with predictable names, insecure permissions, or without proper cleanup. Java provides Files.createTempFile() and File.createTempFile() methods that should be used instead of manual file creation in shared directories.

Both create the file exclusively, so no other process can be holding that path first, and both draw the name from SecureRandom in OpenJDK - File.java for File.createTempFile(), TempFileHelper.java for Files.createTempFile(). That is a property of the implementation, not of the specification: the javadoc promises only that the file did not exist beforehand and that the name will not repeat within the JVM run, saying nothing about how the "internally-generated characters" are chosen. The distinction has mattered: until JDK-6721753, "File.createTempFile produces guessable file names" was fixed in 2008, the name was an incrementing counter seeded from java.util.Random, so observing one name predicted the next. Rely on the exclusive creation, which is specified, and treat the unpredictable name as the defence in depth it is.

On POSIX filesystems both also create the file at 0600 already: Files.createTempFile() applies a default posix:permissions attribute when the caller supplies none (and 0700 for createTempDirectory()), and File.createTempFile() opens with mode 0600. A Files.setPosixFilePermissions() call afterwards is therefore confirmation rather than the fix - useful if you want the mode asserted explicitly, but it is not what closes the window, and code that relies on that ordering elsewhere does have a window.

Primary Defence: Use Files.createTempFile(), passing the permissions as a FileAttribute when you want them stated explicitly, and delete the file in a finally block or with try-with-resources rather than relying on deleteOnExit().

Common Vulnerable Patterns

Predictable filename in shared directory

import java.io.*;

// VULNERABLE - Predictable filename
public class InsecureTemp {
    public void saveUserData(String userData) throws IOException {
        // Predictable filename using PID
        long pid = ProcessHandle.current().pid();
        File tempFile = new File("/tmp/userdata_" + pid + ".txt");

        // Anyone can predict this filename
        try (FileWriter writer = new FileWriter(tempFile)) {
            writer.write(userData);
        }

        processFile(tempFile);
        // File not deleted - persists in /tmp
    }
}

Why this is vulnerable: Predictability matters here because of what it lets an attacker do before the application runs, not what it lets them read afterwards. A local account that can work out the path creates a symbolic link at it first; the application's own open() then follows the link and writes the data wherever the attacker pointed it, with the application's privileges. Disclosure is the mild outcome - the same primitive appends to a file the attacker cannot write to directly.

Java offers no way to open a file exclusively through FileOutputStream, which is why this pattern persists: the constructor truncates whatever it finds. Files.newOutputStream(path, StandardOpenOption.CREATE_NEW) is the call that fails instead, and Files.createTempFile() does both the naming and the exclusive creation.

Fixed filename

import java.io.*;

// VULNERABLE - Fixed filename, race condition
public void exportCredentials(String apiKey, String secret) throws IOException {
    File tempFile = new File("/tmp/credentials.txt");

    // Multiple processes might use same filename
    // No atomic creation - race condition
    try (FileWriter writer = new FileWriter(tempFile)) {
        writer.write("API_KEY=" + apiKey + "\n");
        writer.write("SECRET=" + secret + "\n");
    }

    // File might have insecure permissions
    // File not cleaned up
}

Why this is vulnerable: Nothing has to be predicted - the attacker creates the path first and waits. The sticky bit on /tmp is the protection usually cited for this and does not apply: it stops unprivileged users deleting or renaming files they do not own, not creating a name nobody has taken.

A fixed name is also a collision between two copies of the application, so this fails without an attacker. Two instances, or two threads, interleave their writes into one file and each reads back a mixture.

Using timestamp for filename

// VULNERABLE - Timestamp-based filename is predictable
public void createTempLog() throws IOException {
    long timestamp = System.currentTimeMillis();
    File tempFile = new File("/tmp/log_" + timestamp + ".txt");

    // Attacker can predict the timestamp
    try (FileWriter writer = new FileWriter(tempFile)) {
        writer.write("Sensitive log data");
    }
}

Why this is vulnerable: System.currentTimeMillis() is millisecond-resolution and its value is broadly known - the attacker needs the window rather than the instant, and pre-creates symlinks across it.

Adding entropy to a derived name is the wrong repair. What makes a temp file safe is that the create either succeeds exclusively or fails, so the name only has to be hard enough to guess that collisions are rare; Files.createTempFile() gives both properties, and a hand-built name gives neither.

Insecure permissions on Windows

import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.*;

// VULNERABLE - World-readable permissions
public void saveWithInsecurePermissions(String data) throws IOException {
    Path tempFile = Paths.get("C:\\Temp\\data.txt");

    // Default permissions may be too permissive
    Files.write(tempFile, data.getBytes());

    // On Windows, file may be readable by Everyone
    // On Unix, depends on umask (often 0644)
}

Why this is vulnerable: Windows has no umask and ignores POSIX modes entirely, so any PosixFilePermissions call throws UnsupportedOperationException there rather than quietly doing nothing - code written for one platform fails on the other, which is how permission handling ends up skipped on both.

The per-user temp directory is what actually protects a Windows temp file: %TEMP% sits under the user's profile and inherits an ACL that excludes other users, so the shared-directory race this CWE is about mostly cannot start. That is a property of where the file is, not of the code, so it disappears the moment a path is hardcoded to something like C:\Temp.

Not cleaning up temp files

import java.io.*;
import java.util.Random;

// VULNERABLE - Temp files accumulate
public String processSensitiveData(String data) throws IOException {
    Random random = new Random();
    File tempFile = new File("/tmp/data_" + random.nextInt(10000) + ".tmp");

    try (FileWriter writer = new FileWriter(tempFile)) {
        writer.write(data);
    }

    String result = analyze(tempFile.getAbsolutePath());
    // File never deleted - sensitive data persists
    return result;
}

Why this is vulnerable: "Temporary" is the intent, not the lifetime. On Unix /tmp is typically cleared at boot or after ten days by systemd-tmpfiles; on Windows %TEMP% is cleared only when someone runs Disk Cleanup. The data survives until then, and is copied into anything that snapshots the filesystem.

deleteOnExit() is the usual fix and is a poor one. It registers the path in a list the JVM processes at normal shutdown, so nothing is removed after a SIGKILL, an OOM kill or a container stop - and because the list only grows, a long-running server accumulates entries for files it deleted itself. Delete in a finally block, or use Files.createTempDirectory() and remove the tree.

Reusing a fixed directory under java.io.tmpdir

// VULNERABLE - a predictable directory in a world-writable root
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.util.Set;

public Path createWorkFile(String data) throws IOException {
    Path tempDir = Paths.get(System.getProperty("java.io.tmpdir"),
                             "myapp-" + System.getProperty("user.name"));

    // DANGEROUS: succeeds whatever is already at that path, and applies the
    // permissions only if it is the call that creates the directory
    if (!Files.exists(tempDir)) {
        Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rwx------");
        Files.createDirectories(tempDir, PosixFilePermissions.asFileAttribute(perms));
    }

    Path tempFile = Files.createTempFile(tempDir, "work_", ".dat");
    Files.write(tempFile, data.getBytes());
    return tempFile;
}

// ATTACK:
// 1. Attacker creates /tmp/myapp-alice before the application first runs,
//    as a directory they own or as a symlink to one
// 2. Files.exists() is true, so nothing is created, no attributes applied,
//    and nothing about the existing directory is checked
// 3. Every temp file the application writes there is readable by them

Why this is vulnerable: The username is not a secret and java.io.tmpdir is world-writable on Unix, so the full path is both predictable and reachable by any local account. Files.createDirectories() is the specific mistake: it returns the path successfully when the directory already exists and applies the FileAttribute argument only to directories it actually creates, so an attacker-owned directory is adopted silently. The Files.exists() guard makes it worse rather than better - it is a separate syscall from the creation, which is exactly the window being exploited. The file creation itself is fine; it is the directory underneath that belongs to somebody else.

Secure Patterns

Using Files.createTempFile (Java 7+)

import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.util.Set;

public class SecureTemp {

    public void processSecureData(String data) throws IOException {
        // Create temp file with secure random name
        Path tempFile = Files.createTempFile("secure_", ".tmp");

        try {
            // Set restrictive permissions (owner only)
            Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-------");
            Files.setPosixFilePermissions(tempFile, perms);

            // Write sensitive data
            Files.write(tempFile, data.getBytes());

            // Process the file
            processFile(tempFile.toFile());

        } finally {
            // Always delete the temp file
            Files.deleteIfExists(tempFile);
        }
    }
}

Why this works:

  • Cryptographically random filenames: Files.createTempFile() draws the name from SecureRandom in OpenJDK, so a local attacker cannot work the path out in advance
  • Automatic location: the file lands in java.io.tmpdir without the caller hardcoding a path
  • Restrictive permissions: rw------- (0600) is the mode Files.createTempFile() has already applied on POSIX; setting it explicitly states the requirement in code rather than leaving it to a default
  • Guaranteed cleanup: the try-finally block deletes the file even when processing throws, so sensitive data does not persist in the temp directory

When to use: Best default choice for Java applications - built in to the JDK, no dependency. The Files.setPosixFilePermissions() call is Unix-only; see Secure permissions on Windows and Unix below for a version that runs on both.

Using File.createTempFile with deleteOnExit

import java.io.*;

public void processWithAutoCleanup(String data) throws IOException {
    // Create temp file with unpredictable name
    File tempFile = File.createTempFile("prefix_", ".tmp");

    // Register for deletion on JVM exit (backup cleanup)
    tempFile.deleteOnExit();

    try {
        // Write data
        try (FileWriter writer = new FileWriter(tempFile)) {
            writer.write(data);
        }

        // Process the file
        processFile(tempFile);

    } finally {
        // Explicit deletion (preferred over deleteOnExit)
        if (tempFile.exists()) {
            tempFile.delete();
        }
    }
}

Why this works:

  • Unpredictable filenames: File.createTempFile() draws the name from SecureRandom in OpenJDK, so the path cannot be worked out in advance
  • Backup cleanup: deleteOnExit() registers the file for deletion when the JVM exits normally - not sufficient alone, because nothing is removed after a crash or a kill signal
  • Immediate cleanup: the explicit delete() in the finally block removes the file as soon as processing finishes, rather than leaving it on disk until the JVM exits
  • Defense in depth: explicit deletion covers normal execution; deleteOnExit() covers a graceful shutdown that interrupts the method before its finally block runs
  • Default restrictive permissions: on POSIX, File.createTempFile() opens the file at 0600 already, so setting the mode afterwards confirms it rather than being what closes the window

When to use: Short-lived processes and batch jobs, where the backup deletion is worth registering. Not a long-running server: deleteOnExit() keeps an entry for every file registered, including the ones the finally block has already deleted.

Creating temp file with custom directory

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.util.Comparator;
import java.util.Set;
import java.util.stream.Stream;

public void createInSecureDirectory(String data) throws IOException {
    FileAttribute<Set<PosixFilePermission>> dirAttrs =
        PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"));
    FileAttribute<Set<PosixFilePermission>> fileAttrs =
        PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------"));

    // Create a fresh workspace for this run. createTempDirectory picks the
    // name and creates the directory, so there is no existing path to adopt.
    Path tempDir = Files.createTempDirectory("myapp-", dirAttrs);

    try {
        Path tempFile = Files.createTempFile(tempDir, "work_", ".dat", fileAttrs);
        Files.write(tempFile, data.getBytes(StandardCharsets.UTF_8));
        processFile(tempFile.toFile());
    } finally {
        try (Stream<Path> tree = Files.walk(tempDir)) {
            tree.sorted(Comparator.reverseOrder()).forEach(path -> {
                try {
                    Files.delete(path);
                } catch (IOException e) {
                    // Log and continue: one undeletable file should not stop
                    // the rest of the tree being removed.
                }
            });
        }
    }
}

Why this works:

  • The directory is created, not assumed: Files.createTempDirectory() generates the name with SecureRandom and creates the directory in the same call, failing rather than reusing a path that already exists
  • Permissions applied at creation: passing PosixFilePermissions.asFileAttribute() means the directory is never briefly visible at the umask default. The same applies to the file, which is why there is no Files.setPosixFilePermissions() call afterwards
  • Directory-level isolation: rwx------ (0700) stops other local accounts listing or traversing the directory, which protects every file inside it whatever their own modes are
  • Cleanup covers the tree: deleting in reverse order removes files before the directories holding them, so the workspace does not outlive the method

The distinction that matters is between creating the directory and merely ensuring it exists, which is what Files.createDirectories() on a fixed path loses (see Reusing a fixed directory under java.io.tmpdir above). createTempDirectory() cannot be used that way: it always creates, so there is never an existing directory for it to adopt.

When to use: Applications that need a dedicated workspace for multiple temporary files with directory-level isolation from other users.

Reusing a temp directory between runs

Where a stable path is genuinely required, prefer a base only the user can write - the user's home directory, $XDG_RUNTIME_DIR, or a service-owned location such as /var/lib/myapp. If it has to live under the shared temp root, create it exclusively and verify anything already there:

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.util.Set;

private static Path openAppDir(Path base, String name) throws IOException {
    Path dir = base.resolve(name);
    Set<PosixFilePermission> ownerOnly = PosixFilePermissions.fromString("rwx------");

    try {
        // createDirectory, not createDirectories: it throws if the path exists.
        return Files.createDirectory(dir, PosixFilePermissions.asFileAttribute(ownerOnly));
    } catch (FileAlreadyExistsException e) {
        // Something is there already - fall through and find out what.
    }

    // NOFOLLOW_LINKS, so a symlink is reported as a symlink rather than
    // silently resolved to whatever it points at.
    PosixFileAttributes attrs =
        Files.readAttributes(dir, PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
    if (!attrs.isDirectory()) {
        throw new IOException(dir + " exists and is not a directory");
    }
    UserPrincipal self = dir.getFileSystem().getUserPrincipalLookupService()
        .lookupPrincipalByName(System.getProperty("user.name"));
    if (!attrs.owner().equals(self)) {
        throw new IOException(dir + " is owned by " + attrs.owner().getName());
    }
    // Reject any group or other bit rather than demanding exactly 0700:
    // mkdir applies the process umask, so a stricter umask can legitimately
    // produce a narrower mode.
    if (attrs.permissions().stream().anyMatch(p -> !p.name().startsWith("OWNER"))) {
        throw new IOException(dir + " is open to other users ("
            + PosixFilePermissions.toString(attrs.permissions()) + ")");
    }
    return dir;
}

Why this works: Files.createDirectory() either creates the directory owner-only or throws FileAlreadyExistsException - there is no path through it that hands back a directory somebody else made. The checks then cover what it cannot distinguish: a directory left by an earlier run of the same application looks exactly like one an attacker planted, so type, ownership and permissions all have to be confirmed before use, and throwing is the right outcome when they do not match. This is a Unix problem specifically - on Windows java.io.tmpdir is already per-user and PosixFileAttributes is unavailable, so the equivalent check reads the ACL instead.

Using try-with-resources and AutoCloseable

import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.util.Set;

public class TempFileResource implements AutoCloseable {
    private final Path tempFile;

    public TempFileResource(String prefix, String suffix) throws IOException {
        this.tempFile = Files.createTempFile(prefix, suffix);

        // Set secure permissions immediately
        try {
            Set<PosixFilePermission> perms = 
                PosixFilePermissions.fromString("rw-------");
            Files.setPosixFilePermissions(tempFile, perms);
        } catch (UnsupportedOperationException e) {
            // Platform doesn't support POSIX (e.g., Windows)
            // File.createTempFile already creates with restricted access on Windows
        }
    }

    public Path getPath() {
        return tempFile;
    }

    public void write(byte[] data) throws IOException {
        Files.write(tempFile, data);
    }

    @Override
    public void close() throws IOException {
        Files.deleteIfExists(tempFile);
    }

    // Usage
    public static void processData(String data) throws IOException {
        try (TempFileResource temp = new TempFileResource("secure_", ".tmp")) {
            temp.write(data.getBytes());
            processFile(temp.getPath().toFile());
        }
        // Automatic cleanup when close() is called
    }
}

Why this works:

  • Automatic resource management: AutoCloseable works with try-with-resources, so close() runs even when the block exits on an exception
  • Unpredictable filenames: the constructor uses Files.createTempFile(), so the name comes from SecureRandom
  • Permissions asserted at construction: Files.createTempFile() has already created the file at 0600 on POSIX; setting them again in the constructor states the requirement in code rather than leaving it to a default that a future refactor could drop
  • Cross-platform compatibility: catching UnsupportedOperationException around the permissions call lets the same class run on Windows, which has no POSIX view
  • Encapsulation: one reusable class holds the temp file's whole lifecycle, so every call site gets the same handling
  • Prevents security gaps: developers cannot accidentally skip the permission setting or the cleanup

When to use: Java applications that create temp files in several places and want one type holding the creation, permissions and cleanup.

Secure permissions on Windows and Unix

import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.util.Set;

public void createSecureMultiplatform(String data) throws IOException {
    Path tempFile = Files.createTempFile("secure_", ".tmp");

    try {
        // Set permissions based on OS
        if (tempFile.getFileSystem().supportedFileAttributeViews().contains("posix")) {
            // Unix/Linux - use POSIX permissions
            Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-------");
            Files.setPosixFilePermissions(tempFile, perms);
        } else {
            // Windows - use ACL
            File file = tempFile.toFile();
            file.setReadable(false, false);  // Remove read for others
            file.setReadable(true, true);    // Set read for owner only
            file.setWritable(false, false);  // Remove write for others
            file.setWritable(true, true);    // Set write for owner only
        }

        Files.write(tempFile, data.getBytes());
        processFile(tempFile.toFile());

    } finally {
        Files.deleteIfExists(tempFile);
    }
}

Why this works:

  • Automatic OS detection: supportedFileAttributeViews().contains("posix") decides which branch runs, so the caller does not test the operating system itself
  • Unix/Linux POSIX permissions: rw------- (0600) provides standard owner-only file access understood by all Unix tools
  • Windows permission handling:
    • setReadable(false, false) and setWritable(false, false) remove permissions for all users
    • setReadable(true, true) and setWritable(true, true) grant permissions only to file owner
  • Cross-platform deployment: the same source runs on Linux, Windows and macOS - neither branch reaches an API the platform does not support

When to use: Applications deployed across different operating systems, where one code path has to produce owner-only access on each.

Using Apache Commons IO TempFile utilities

import org.apache.commons.io.FileUtils;
import java.io.*;
import java.nio.file.*;

public void useCommonsIO(String data) throws IOException {
    File tempFile = File.createTempFile("prefix_", ".tmp");
    tempFile.deleteOnExit();

    try {
        // Write data using Commons IO
        FileUtils.writeStringToFile(tempFile, data, "UTF-8");

        // Set secure permissions
        tempFile.setReadable(false, false);
        tempFile.setReadable(true, true);
        tempFile.setWritable(false, false);
        tempFile.setWritable(true, true);

        processFile(tempFile);

    } finally {
        FileUtils.deleteQuietly(tempFile);
    }
}

Why this works:

  • Convenient wrappers: Apache Commons IO's FileUtils wraps the JDK file calls without changing what they do
  • Unpredictable filenames: File.createTempFile() generates cryptographically random names
  • Backup cleanup: deleteOnExit() provides deletion guarantee on normal JVM shutdown
  • Proper encoding: FileUtils.writeStringToFile() takes the charset explicitly (UTF-8) rather than leaving it implicit
  • Permission restrictions: setReadable() and setWritable() restrict access to owner only
  • Robust deletion: FileUtils.deleteQuietly() attempts deletion without throwing exceptions if file doesn't exist

Note: Convenient where the application already depends on Commons IO. setReadable() and setWritable() have different semantics on Windows than POSIX permissions do; where that matters, set the Windows ACL explicitly.

Temporary directory management

import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.util.Comparator;
import java.util.Set;
import java.util.stream.Stream;

public class TempDirectoryManager implements AutoCloseable {
    private final Path tempDir;

    public TempDirectoryManager(String prefix) throws IOException {
        // Create the directory with its permissions, in one call
        Path dir;
        try {
            Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rwx------");
            dir = Files.createTempDirectory(prefix, PosixFilePermissions.asFileAttribute(perms));
        } catch (UnsupportedOperationException e) {
            // Windows: no POSIX permissions. java.io.tmpdir is per-user there
            // and the new directory inherits its ACL.
            dir = Files.createTempDirectory(prefix);
        }
        this.tempDir = dir;
    }

    public Path createFile(String name, byte[] data) throws IOException {
        Path file = tempDir.resolve(name);

        // Create with owner-only permissions before writing, rather than
        // writing first and tightening afterwards
        try {
            Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-------");
            Files.createFile(file, PosixFilePermissions.asFileAttribute(perms));
        } catch (UnsupportedOperationException e) {
            // Windows - the file inherits the directory's ACL
            Files.createFile(file);
        }

        Files.write(file, data);
        return file;
    }

    @Override
    public void close() throws IOException {
        // Delete directory and all contents
        if (tempDir != null && Files.exists(tempDir)) {
            try (Stream<Path> tree = Files.walk(tempDir)) {
                tree.sorted(Comparator.reverseOrder())
                    .forEach(path -> {
                        try {
                            Files.delete(path);
                        } catch (IOException e) {
                            // Log error
                        }
                    });
            }
        }
    }

    // Usage
    public static void processMultipleFiles() throws IOException {
        try (TempDirectoryManager manager = new TempDirectoryManager("workdir_")) {
            Path file1 = manager.createFile("data1.txt", "content1".getBytes());
            Path file2 = manager.createFile("data2.txt", "content2".getBytes());

            batchProcess(file1, file2);
        }
        // All files and directory automatically deleted
    }
}

Why this works:

  • Complete lifecycle management: AutoCloseable removes the whole directory tree when the try-with-resources block exits, including when it exits on an exception
  • Cryptographically random directory names: Files.createTempDirectory() generates unpredictable names with restrictive permissions (0700 on Unix with PosixFilePermissions attributes)
  • Directory-level protection: every file the manager creates sits inside the 0700 directory, so other local accounts cannot list or traverse it whatever the files' own modes are
  • Multiple file support: related temp files share one workspace and one cleanup, rather than each being tracked separately
  • Reverse deletion order: Files.walk() with sorted(Comparator.reverseOrder()) ensures files deleted before parent directories (required for successful directory deletion)

When to use: Processing that needs several temporary files at once, such as a batch job writing intermediate files - one place manages their lifecycle and permissions.

Spring Framework temporary file handling

import org.springframework.http.ResponseEntity;
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;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

@RestController
public class FileUploadController {

    @PostMapping("/upload")
    public ResponseEntity<String> handleFileUpload(
            @RequestParam("file") MultipartFile file) throws IOException {

        // SECURE - the file created here is the file written to
        // createTempFile() creates exclusively, and at 0600 on POSIX
        Path tempFile = Files.createTempFile("upload_", ".tmp");

        try {
            // Write into the existing file rather than handing its path to
            // something that may replace it - see the note below
            try (InputStream in = file.getInputStream();
                 OutputStream out = Files.newOutputStream(tempFile,
                         StandardOpenOption.WRITE,
                         StandardOpenOption.TRUNCATE_EXISTING)) {
                in.transferTo(out);
            }

            // Validate and process
            if (!isValidFile(tempFile)) {
                return ResponseEntity.badRequest().body("Invalid file");
            }

            String result = processUploadedFile(tempFile);
            return ResponseEntity.ok(result);

        } finally {
            Files.deleteIfExists(tempFile);
        }
    }
}

Why this works:

  • The file that was created is the file that is written: MultipartFile.transferTo(File) is specified to delete an existing destination first, and the servlet-backed implementation may satisfy it by moving the spooled upload into place. Either way the destination is a new file created with the umask default, so the temp file created above - and any mode set on it - is discarded. Opening an OutputStream on the existing path keeps the original file and truncates it in place
  • Unpredictable filename, exclusive creation: Files.createTempFile() fails rather than reusing a path an attacker got to first, and on POSIX it creates at 0600 without a separate permissions call - which also means no UnsupportedOperationException when the same code runs on Windows
  • Pre-processing validation: the file is validated (type, size, content) before anything downstream reads it
  • Guaranteed cleanup: try-finally deletes the temp file even when validation fails or processing throws

Spring already writes uploads to a temp file of its own. MultipartProperties controls it: spring.servlet.multipart.location sets the directory, and max-file-size/max-request-size bound what gets spooled there before your handler runs. Point location at a directory the application owns rather than leaving it at the container default under the shared temp directory - the code above cannot protect a file Spring wrote before it was called.

When to use: Spring Boot applications handling file uploads. For production, also consider virus scanning and storing valid files in a secure permanent location.

Common Pitfalls

  • A downstream copy silently reverting to default permissions: Setting POSIX permissions with Files.setPosixFilePermissions() on the original temp file, but a later step (Files.copy() to "normalize" it, or a library that writes to a new file with the same name) creates a fresh file that gets the default umask-derived permissions instead of the explicit rw-------.
  • Relying on deleteOnExit() alone in a long-running server application: File.deleteOnExit() only deletes on a normal JVM shutdown and accumulates an internal list for the life of the process. In a long-running app that creates many temp files, this both leaks memory (the deletion list never shrinks until exit) and leaves files on disk indefinitely if the JVM is killed or restarted.
  • Swapping a predictable filename for UUID.randomUUID() string concatenation instead of Files.createTempFile(): new File(tmpDir, UUID.randomUUID() + ".tmp") produces an unpredictable name, but unlike Files.createTempFile() it doesn't create the file atomically/exclusively - using FileWriter afterward happily creates-or-truncates, reopening the same race condition the unpredictable name was meant to close.

Additional Resources