Why temporary files are a security concern

Temporary files are commonly used for:

  • buffering uploads before processing
  • staging generated reports
  • passing data to external tools
  • storing intermediate artifacts during batch jobs

The risk appears when code uses a predictable filename such as /tmp/output.txt, or when it creates a file in a shared directory and then opens it later by name. An attacker who can write to the same directory may pre-create a symlink, race your process, or read data before cleanup.

Common failure modes include:

RiskWhat can go wrongSafer approach
Predictable namesAttacker guesses the path and interferesUse randomized names from a temp-file API
Symlink attacksFile creation follows a malicious linkCreate files atomically with exclusive semantics
World-readable permissionsSensitive data is exposed to other usersRestrict permissions to the current user
Cleanup bugsTemporary data persists after failureUse RAII-based cleanup and scoped lifetimes
Reopening by pathPath can be swapped after creationKeep and use the open file handle

The key idea is simple: treat temporary files as security-sensitive resources, not just disposable scratch space.

Prefer scoped temporary files and directories

The safest default is to use a library that creates unique names and removes files automatically when they go out of scope. The tempfile crate is the standard choice in Rust for this pattern.

Basic example

use std::io::{self, Write};
use tempfile::NamedTempFile;

fn main() -> io::Result<()> {
    let mut temp = NamedTempFile::new()?;
    writeln!(temp, "intermediate data")?;

    // The file exists on disk, but only this process has the handle.
    println!("Temp file path: {}", temp.path().display());

    // Automatically deleted when `temp` is dropped.
    Ok(())
}

This is safer than manually constructing a path because:

  • the filename is random and hard to guess
  • creation is atomic
  • cleanup happens automatically on drop
  • the file handle remains valid even if the path is removed

For temporary directories, use tempfile::TempDir. This is especially useful when a tool needs a directory tree with multiple files.

use std::fs::File;
use std::io::{self, Write};
use tempfile::TempDir;

fn main() -> io::Result<()> {
    let dir = TempDir::new()?;
    let file_path = dir.path().join("config.json");

    let mut file = File::create(&file_path)?;
    file.write_all(br#"{"mode":"safe"}"#)?;

    // Directory and contents are removed when `dir` is dropped.
    Ok(())
}

Avoid predictable paths in shared locations

A common anti-pattern is writing to a fixed location in /tmp or another shared directory:

use std::fs::File;
use std::io::Write;

fn unsafe_example() {
    let mut file = File::create("/tmp/report.txt").unwrap();
    writeln!(file, "secret report data").unwrap();
}

This is dangerous because another process may already have created /tmp/report.txt as a symlink or hard link. Even if the file is not maliciously pre-created, a second process may read or overwrite it.

Instead, create the file with a random name and keep the handle open. If you need a path for interoperability, use a secure temp API to generate it.

When a path must be shared

Sometimes another process must read the file by path, such as a legacy tool or external command. In that case:

  1. create the file securely first
  2. write the contents
  3. set restrictive permissions
  4. pass the path only to trusted consumers
  5. remove the file as soon as it is no longer needed

If the consumer can accept a file descriptor or inherited handle, that is even better than sharing a path.

Use restrictive permissions for sensitive data

Temporary files often contain tokens, decrypted payloads, private keys, or user uploads. On Unix-like systems, the default process umask may still allow broader access than intended. On Windows, ACL behavior differs, but the principle remains: do not assume temporary files are private unless you explicitly make them so.

With tempfile, the initial creation is generally safe, but if you create files manually you should set permissions immediately after creation. For Unix targets, use PermissionsExt when needed.

use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::os::unix::fs::PermissionsExt;

fn main() -> io::Result<()> {
    let path = "/tmp/private-workfile";

    let mut file = OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(path)?;

    file.write_all(b"top secret")?;

    fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
    Ok(())
}

Better yet, avoid manual permission management unless you have a specific interoperability requirement. Libraries that encapsulate secure creation are less error-prone.

Keep data on the open handle, not the path

A subtle security issue appears when code creates a temporary file securely, closes it, and later reopens it by path. Between those operations, the path may be replaced.

This pattern is safer:

  • create the file
  • write to it through the same handle
  • pass the handle or its contents forward
  • let the object clean itself up

This pattern is less safe:

  • create file
  • close file
  • store path in a variable
  • reopen later by path

If you need to hand data to another function, prefer passing a File or a reader/writer trait object rather than a string path. That keeps the trust boundary narrow.

Example: processing with an open file

use std::fs::File;
use std::io::{self, Read, Write};
use tempfile::NamedTempFile;

fn process_file(mut file: File) -> io::Result<String> {
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

fn main() -> io::Result<()> {
    let mut temp = NamedTempFile::new()?;
    write!(temp, "hello from a secure temp file")?;

    let file = temp.reopen()?;
    let output = process_file(file)?;
    println!("{output}");

    Ok(())
}

The important detail is that the file is opened securely once, and the same underlying object is used for reading.

Be careful when invoking external tools

Temporary files are often used as inputs to command-line tools. That can be safe, but only if the file path and lifecycle are controlled.

Consider these best practices:

  • create the temp file before invoking the tool
  • keep the file private until the tool needs it
  • avoid placing temp files in directories writable by untrusted users
  • delete the file immediately after use
  • prefer passing stdin/stdout when the tool supports it

If the tool accepts input from standard input, that is usually safer than writing a temp file at all. If it requires a file path, use a secure temp file object and pass its path only for the duration of the call.

Use temp directories for multi-file workflows

Some workflows need a directory tree rather than a single file, such as unpacking a report bundle or generating several related artifacts. In that case, use TempDir instead of manually creating a directory under /tmp.

A temp directory helps you avoid collisions and makes cleanup deterministic. It also reduces the chance that unrelated files in the same location affect your logic.

use std::fs::{self, File};
use std::io::{self, Write};
use tempfile::TempDir;

fn main() -> io::Result<()> {
    let dir = TempDir::new()?;
    let input = dir.path().join("input.txt");
    let output = dir.path().join("output.txt");

    File::create(&input)?.write_all(b"sample data")?;
    fs::copy(&input, &output)?;

    println!("Working directory: {}", dir.path().display());
    Ok(())
}

If you need the directory to survive beyond the current scope, TempDir::into_path() can persist it. Use that sparingly and only when you have a clear cleanup strategy, because persistence weakens the safety guarantees.

Handle cleanup failures explicitly

Automatic cleanup is useful, but it is not a substitute for error handling. A temp file may fail to delete because of permission issues, open handles, antivirus interference, or filesystem problems. In security-sensitive code, you should decide what failure means.

For example:

  • if the file contains secrets, consider overwriting or encrypting it before persistence
  • if cleanup fails, log the event and alert operators
  • if the file is not sensitive, a cleanup failure may be acceptable but should still be visible

The tempfile crate handles the common case well, but your application should still treat cleanup as part of the security model rather than an afterthought.

Choose the right temporary storage strategy

Not every temporary artifact needs the same level of protection. The following table can help you choose a pattern.

Use caseRecommended approachNotes
Short-lived scratch dataNamedTempFileBest default for single-file workflows
Multi-step processingTempDirGood for several related files
Data passed to another processSecure temp file + immediate usePrefer stdin if possible
Sensitive materialOpen handle + restrictive permissionsMinimize path exposure
Large transient blobsTemp file on a trusted filesystemWatch disk quotas and cleanup

The more sensitive the data, the more you should prefer scoped, handle-based APIs over path-based ones.

Practical checklist

Before shipping code that uses temporary files, verify the following:

  • file names are randomized, not predictable
  • creation is atomic and exclusive
  • permissions are restrictive enough for the data
  • the file is used through an open handle when possible
  • cleanup happens automatically or is explicitly managed
  • temp paths are not exposed to untrusted users
  • external tools receive the file only for the minimum necessary time

If you can answer “yes” to all of these, your temporary file usage is likely robust enough for production.

Conclusion

Temporary files are easy to overlook because they feel transient, but they can still leak secrets, enable races, or create privilege boundaries if handled carelessly. In Rust, the safest approach is to use scoped abstractions like NamedTempFile and TempDir, keep data on open handles, and avoid predictable paths in shared directories.

When you need manual control, create files atomically, restrict permissions, and treat the path as sensitive. That mindset turns temporary storage from a common source of bugs into a well-contained implementation detail.

Learn more with useful resources