How do I create a temporary file in a secure manner?

This question is distressingly difficult to answer, because there isn't any single command that simply works everywhere. This page will discuss the difficulties involved, and will offer a variety of solutions. Which one you choose will depend on your script's needs and expected runtime environment.

The issues

Traditionally, Unix and Unix-like systems have offered one or two world-writable directories (/tmp and sometimes /var/tmp), where all programs are expected to create their temporary files. This is convenient for performing cleanups of leftover files, but from a security perspective, it's a disaster. A malicious program can create a file or symbolic link that another program may use unwittingly.

Some programs may run as a user account that doesn't have its own private home directory, or any other location where files may be created without disturbance by other programs. These programs have nowhere else to create their files except for /tmp or /var/tmp.

In order to create a temporary file safely in a world-writable directory, a program must perform an atomic file creation, so that there will not be a RaceCondition window during which a malicious program may insert its own version of the file. The tools needed to do this in a shell script are not consistently available on all platforms. Some systems have tempfile. Others have mktemp. Some of the mktemp systems (e.g. HP-UX) have a broken version which doesn't create the file by default -- it simply spits out a name (which is not secure). Such systems may have mktemp -c to create the file properly, but the -c option isn't recognized on the systems where mktemp works properly by default. A few systems (e.g. Solaris) don't have either tempfile or mktemp.

The creation of a temporary file name will typically involve the use of a pseudorandom number generator. Some shells offer a $RANDOM variable which gives RNG capability, but the POSIX shell specification does not require this to exist.

The concatenation of random numbers to create a file name may result in a name that is too long for the file system mounted on /tmp. Some extremely old file systems have a 14-character file name length limit.

Solution 1: Use your home directory.

The best portable answer is to put your temporary files in your home directory (or some other private directory, e.g. $XDG_RUNTIME_DIR) where nobody else has write access. Then at least you don't have to worry about malicious users. Simplistic PID-based schemes (or hostname + PID for shared file systems) should be enough to prevent conflicts with your own scripts.

If you're implementing a daemon which runs under a user account with no home directory, why not simply make a private directory for your daemon at the same time you're installing the code?

Unfortunately, some people don't seem to like that answer. They either don't have the privileges to create a directory for their script to use, or for various reasons they insist that their temporary files must reside in /tmp or /var/tmp. For those people, there is no clean answer, so they must choose a hack they can live with.

Solution 2: Create a private directory at runtime.

If you can't use $HOME, the next best answer is to create a private directory to hold your temp file(s), instead of creating the files directly inside a world-writable sandbox like /tmp or /var/tmp. On some systems, the mktemp command has a -d option to create a directory, rather than a file. If you're fortunate enough to have this option, using it is simple:

   1 # POSIX sh with mktemp -d available
   2 
   3 tempdir=""
   4 cleanup() { test "$tempdir" && rm -rf -- "$tempdir"; }
   5 trap cleanup EXIT HUP INT TERM    # in Bash you can just trap cleanup EXIT
   6 tempdir=$(mktemp -d -- "${TMPDIR:-/tmp}/XXXXXXXXXXXXXX") ||
   7   { echo 'Error creating a temporary directory' >&2; exit 1; }
   8 
   9 # Don't forget to call "cleanup" at the end of your script, if you aren't in bash.

Then you can create your files inside the temporary directory.

If you don't have mktemp -d, then the solution becomes more complicated. The mkdir command is atomic, and only reports success if it actually creates the directory. So long as we do not use the -p option, we can be assured that it actually creates a brand new directory, rather than following a symlink to danger.

Here is one example of this approach:

   1 # Bash
   2 
   3 tempdir=""
   4 trap '[[ $tempdir ]] && rm -rf -- "$tempdir"' EXIT
   5 
   6 i=0
   7 while ((++i <= 10)); do
   8   tempdir=${TMPDIR:-/tmp}/$RANDOM-$$
   9   mkdir -m 700 -- "$tempdir" 2>/dev/null && break
  10 done
  11 
  12 if ((i > 10)); then
  13   echo 'Could not create temporary directory' >&2
  14   exit 1
  15 fi

Instead of RANDOM, awk can be used to generate a random number in a POSIX compatible way:

   1 # POSIX sh
   2 
   3 i=0
   4 tempdir=""
   5 cleanup() {
   6   [ "$tempdir" ] && rm -rf -- "$tempdir"
   7   if [ "$1" != EXIT ]; then
   8     trap - "$1"          # reset trap, and
   9     kill -s "$1" -- "$$"    # resend signal to self
  10   fi
  11 }
  12 for sig in EXIT HUP INT TERM; do
  13   trap "cleanup $sig" "$sig"
  14 done
  15 
  16 # Generate 10 random 6-digit numbers, separated by commas
  17 rand=$(
  18   awk 'BEGIN { srand(); for (i = 0; i < 10; i++) printf("%06d,", 1e6*rand()) }'
  19 )
  20 while [ "$rand" ]; do
  21   tempdir=${TMPDIR:-/tmp}/${rand%%,*}-$$
  22   mkdir -m 700 -- "$tempdir" 2>/dev/null && break
  23   rand=${rand#*,}
  24 done
  25 
  26 # if all random numbers have been exhausted
  27 if [ -z "$rand" ]; then
  28   printf 'Could not create temporary directory\n' >&2
  29   exit 1
  30 fi

Note that srand() seeds the random number generator using seconds since the epoch which is fairly easy for an adversary to predict and perform a denial of service attack. (Historical awk implementations that predate POSIX may not even use the time of day for srand(), so don't count on this if you're on an ancient system.)

Some systems have a 14-character filename limit, so avoid the temptation to string $RANDOM together more than twice. You're relying on the atomicity of mkdir for your security, not the obscurity of your random name. If someone fills up /tmp with hundreds of thousands of random-number files to thwart you, you've got bigger issues.

If you don't care for awk's srand(), you can try to bring in randomness from other sources. Some systems have a /dev/random or /dev/urandom device which provides random bytes. These bytes can be turned into a filename:

   1 # POSIX sh with /dev/random available
   2 randname() {
   3   dd bs=6 count=1 if=/dev/random 2>/dev/null | od -tx1 -An | tr -d ' '
   4 }
   5 
   6 # Replace the tempdir assignment in the previous example.
   7 while [ "$i" -lt 10 ]; do
   8   tempdir=${TMPDIR:-/tmp}/$(randname)-$$

If you don't have /dev/random or any equivalent interface, you can pull entropy from commands which are likely to give highly volatile output, and use a hash function to turn the entropy into a filename. POSIX doesn't provide a large set of hashing tools, but it does have cksum.

   1 # POSIX sh
   2 randname() {
   3   { date; who; ps -ef; ps auxw; } 2>&1 | cksum | tr -d ' ' | cut -c1-14
   4 }
   5 
   6 while [ "$i" -lt 10 ]; do
   7   tempdir=${TMPDIR:-/tmp}/$(randname)-$$

The use of both ps commands tries to ensure that at least one of them will work. If they both work, that's even better. Feel free to add other highly volatile commands, if your system has them.

Solution 3: Use platform-specific tools

If you're writing a script for only a specific set of systems, and if those systems have a mktemp or tempfile tool which works correctly, you can use that. Make sure that your tool actually creates the temporary file, and does not simply give you an unused name. The options will vary from system to system, so you will have to write your script with that in mind (see e.g. MacOS Darwin 2019 notes). Since some legacy systems may not have these tools, this is not guaranteed to be a portable solution. But it's often good enough for most operating systems.

Here's an example using GNU's mktemp:

   1 # POSIX sh
   2 
   3 tmpfile=""
   4 trap '[ "$tmpfile" ] && rm -f -- "$tmpfile"' EXIT HUP INT TERM
   5 tmpfile=$(mktemp)

The mktemp command is the most likely platform-specific tool to exist. It is widely available (GNU coreutils since 2007), and there has been discussion about its status in POSIX (see StackExchange 2020). However, some old versions of mktemp may require a -c option to create the file in advance, or they may create the file by default and fail if -c is supplied.

The tempfile is also not portable, but you may find it on some systems that lack mktemp.

Some legacy systems, like Solaris, may not have either command.

In POSIX systems, you could possibly revert to m4, which has the ability to create a temporary file. However, the command may not be installed by default, and old m4 versions may be missing this feature.

Solution 4: Use m4.

The m4 approach, although in POSIX, may not be an ideal solution in practice. For example, the m4 in MacOS (July 2021) was too old to have the mkstemp macro included. Note that mkstemp requires a template, including a path prefix, or else it creates the temporary file in the current directory.

   1 # Bash
   2 
   3 die() { echo "$*" >&2; exit 1; }
   4 
   5 unset -v tmpfile
   6 trap '[ "$tmpfile" ] && rm -f -- "$tmpfile"' EXIT
   7 tmpfile=$(m4 - <<< 'mkstemp(/tmp/foo-XXXXXX)') ||
   8   die "couldn't create temporary file"

Solution 5: Try to create a temporary file manually.

The traditional approach:

# THIS IS A BAD EXAMPLE!  DO NOT USE THIS!
# Problems: race condition, security
tempfile=/tmp/myname.$$
trap 'rm -f -- "$tempfile"; exit 1' HUP INT QUIT TERM
rm -f -- "$tempfile"
touch -- "$tempfile"

The problem with this is: if the file already exists (for example, as a symlink to /etc/passwd), then the script may write things in places they should not be written. Even if you remove the file immediately before using it, you still have a RaceCondition: someone could re-create a malicious symlink in the interval between your shell commands.

To make this work safely, we must perform an atomic file creation. The way to do this in a shell script is to turn on the noclobber option (set -C) and then attempt an output redirection. If the redirection fails, try again with a different name, until it succeeds, or until too many tries have already been attempted.

   1 # POSIX sh
   2 
   3 mktmpfile() {
   4   (
   5     umask 077; set -C
   6     i=0
   7     while test $i -lt 10; do
   8       f=${TMPDIR:-/tmp}/ME-$$-$i
   9       ( : > "$f" ) >/dev/null 2>&1 && { printf '%s\n' "$f"; exit 0; }
  10       i=$((i+1))
  11     done
  12     exit 1
  13   )
  14 }
  15 
  16 tmpfile=
  17 trap 'test "$tmpfile" && rm -f -- "$tmpfile"' EXIT HUP INT TERM
  18 tmpfile=$(mktmpfile) || { echo >&2 'Error creating temp file'; exit 1; }

If $RANDOM or some other source of random numbers is available, you may use that when generating the file names. The randname() functions from Solution 2 may also be used here, for example.

Solution 6: Use other approaches.

Another not-quite-serious suggestion is to include C code in the script that implements a mktemp(1) command based on the mktemp(3) library function, compile it, and use that in the script. But this has a couple problems:

BashFAQ/062 (last edited 2025-02-09 13:06:43 by emanuele6)