Articles / Linux

  • linux
  • shell
  • mktemp
  • security
  • privacy
  • sysadmin

Your Shell Script's Temp Files Are a Privacy Leak; mktemp Fixes It

Somewhere in your scripts there is probably a line like curl ... > /tmp/out.$$. It works, it is quick to write, and on a shared machine it hands your data to anyone who looks. The file is world-readable, its name is guessable, and nothing cleans it up if the script dies.

What the classic pattern gets wrong

Take this fairly typical script:

#!/bin/sh
f=/tmp/report.$$
grep -r "customer" /var/app/exports > "$f"
sort "$f" | mail -s "report" [email protected]
rm "$f"

There are three separate problems hiding in it:

  • Readable by everyone. With the usual umask of 022, the file is created mode 0644. Any local user can read it while it exists.
  • Predictable name. $$ is the process ID, which anyone can watch via ps or just guess. Another user can create that name first.
  • No cleanup on failure. If mail fails, or you hit Ctrl-C, the rm never runs and the data sits in /tmp.

The pre-created symlink trick

The predictable name is the dangerous bit. An attacker who can guess /tmp/report.4711 creates a symlink there pointing at something you own, say ~/.profile. Your > redirection opens the path, follows the link and truncates your real file.

Many modern distributions set fs.protected_symlinks, which blocks following such links in sticky world-writable directories like /tmp in many cases. That is a kernel mitigation for a script bug, though, and it does not stop an attacker simply creating a plain file with a permissive mode first, then reading what you write into it.

What mktemp does differently

Run mktemp with no arguments and you get a fresh, empty file with an unpredictable name, mode 0600, owned by you. Run mktemp -d and you get a directory with mode 0700.

$ f=$(mktemp)
$ ls -l "$f"
-rw------- 1 andy andy 0 Nov 11 09:14 /tmp/tmp.k3Qp9aZ1Lc
$ d=$(mktemp -d)
$ ls -ld "$d"
drwx------ 2 andy andy 4096 Nov 11 09:14 /tmp/tmp.WbT7nRd2Fs

The exact name format varies between implementations, but the properties are the same.

Quick detour: why a collision can't hurt you

Hang on, why is that safe if names are only random, not secret? Because mktemp creates the file with open() using O_CREAT | O_EXCL. That call fails if anything already exists at the path, and it refuses to follow a symlink in the final component. If an attacker got there first, mktemp just picks another name and tries again.

The check and the creation are one atomic kernel operation. The old shell habit of "test whether it exists, then write" has a gap between the two steps, and that gap is exactly what attackers use.

Clean up with a trap

A safe file is only half of it. Remove it on every exit path:

#!/bin/sh
set -eu
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
trap 'exit 1' HUP INT TERM

grep -r "customer" /var/app/exports > "$tmp/out"
sort "$tmp/out" | mail -s "report" [email protected]

Using one temporary directory rather than several files means a single rm -rf covers everything, and you can name files inside it as you like. The second trap matters because in some shells a signal kills the script without running the EXIT trap; turning the signal into a normal exit makes it fire.

A trap cannot catch SIGKILL, and it will not run if the machine loses power. If the contents are truly sensitive, do not rely on cleanup alone.

Where the file lives matters too

By default mktemp uses $TMPDIR if set, otherwise /tmp. On many systems /tmp is on disk, so deleted contents may linger in free blocks until overwritten. For secrets there are two better choices:

  • Set TMPDIR="$XDG_RUNTIME_DIR" for the call. On systemd machines this is a per-user tmpfs, mode 0700, held in RAM.
  • Better still, avoid the file. A pipe or a here-document keeps the data in memory, subject to the usual caveat that swap can page RAM to disk.
tmp=$(TMPDIR="${XDG_RUNTIME_DIR:-/tmp}" mktemp -d)

Other ways temp data leaks

Do not put the secret in a command line to avoid the file. Arguments are visible to every user through ps and /proc/PID/cmdline. Read it from standard input or a 0600 file instead.

Also check what you keep inside the temp directory. A "temporary" dump of a database table is still personal data, and if it was written to a backed-up or snapshotted path, it can outlive your rm by a long way.

A quick check for your own scripts

Search for the patterns that give the game away:

grep -rnE '/tmp/[A-Za-z0-9_.-]*\$\$|/tmp/[a-z_.-]+["> ]' /usr/local/bin ~/bin

Anything that builds a fixed or PID-based name in /tmp is a candidate. Swap in mktemp, add the trap, and the three problems from the start go away: the mode is 0600, the name is unguessable, and the cleanup runs on failure.