Why is a temp file path built from tempfile.gettempdir() and a fixed name unsafe?
answer
- A shared directory with guessable names
- The gap between checking and creating
- Two runs, one filename, wrong totals
- Something else is waiting at that path
- Exclusive create at 0o600 closes it
basics
~20 sThe system temp directory is shared and world-writable, so a guessable name is a race: another process can create that path first, often as a symlink, and your write lands somewhere else. It also makes concurrent runs of your own job collide.
solid answer
~50 sComposing a path yourself leaves a window between deciding on a name and creating the file. In a shared temp directory a hostile local process can win that window and plant a symlink at your path, so your open follows it and writes into a file you never intended — the classic time-of-check-to-time-of-use race. `tempfile.mktemp()` is unsafe for exactly this reason and should never be used. `tempfile.mkstemp()` closes the window by calling `os.open` with the exclusive-create flag and mode `0o600` in one atomic step, retrying on collision, so the file cannot already exist and cannot be a symlink to somewhere else. The same fixed name is also a plain concurrency bug: two runs of the job stomp on each other's data with no attacker involved. Let `tempfile` name the file, or give each run its own `TemporaryDirectory`.
code
python · 27 linesimport os
import tempfile
TMP = tempfile.gettempdir()
def stage_unsafe(rows, path=os.path.join(TMP, "recon-batch.csv")):
with open(path, "w", encoding="utf-8") as f:
f.writelines(rows)
return path
print("every call shares one name:", stage_unsafe([]) == stage_unsafe([]))
os.unlink(os.path.join(TMP, "recon-batch.csv"))
def stage_safe(rows):
fd, path = tempfile.mkstemp(prefix="recon-batch-", suffix=".csv")
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.writelines(rows)
return path
a, b = stage_safe([]), stage_safe([])
print("each call gets its own:", a != b)
for p in (a, b):
os.unlink(p)go deeper
Recall the rule rather than the theory: never build a temp path by joining the temp directory with a name you chose. Call tempfile.NamedTemporaryFile or TemporaryDirectory and let the module pick the name.
Explain the mechanics of the race — that checking for existence and then creating are two system calls with a schedulable gap — and that mkstemp collapses them into one exclusive create at mode 0o600 that also refuses to follow a symlink.
Demonstrate that you can spot this in review and in an incident: a shared staging path that corrupts concurrent runs, a symlink attack on a shared box, and the fix of giving each run its own directory rather than a cleverer filename.
Own the systemic angle: whether services get private temp namespaces, whether sensitive intermediates belong in shared temp space at all, and how a lint rule or review checklist stops the hand-built path pattern reappearing across many repositories.
## Two failures wearing one costume A predictable temporary path fails in two different ways, and a good answer separates them, because they have different audiences and different severities. **The correctness failure.** Say a payment reconciliation job has a staging helper whose destination is computed once, as a default argument evaluated at definition time, and therefore shared by every call and every concurrent run of the process. At month end a four-person team all kick the job off by hand within the same minute; four runs write the same staging file, each partially overwriting the others, and the totals that come out are wrong in a way that reproduces on nobody's laptop. No attacker, no security bug, just one name where there should have been many. **The security failure.** The system temp directory is shared by every user on the machine and is world-writable. If your name is guessable — `recon-batch.csv`, or anything derived from the process id, the date, or a counter — a local process running as another user can create that path before you do. The usual weapon is a symbolic link: it points your path at a file the attacker cannot write but you can, and your innocent `open(path, "w")` follows the link and truncates the target with your privileges. The mirror trick is to plant a file the attacker *can* read, so that whatever your job stages there leaks. Where the temp directory holds sensitive intermediates — and a reconciliation run's staging file is exactly that — both directions matter. ## Why check-then-create cannot fix it The instinct is to guard the write: check whether the path exists, and only create it if it does not. That is a time-of-check-to-time-of-use race. Between the check returning "absent" and your `open` running, the operating system may schedule the attacker's process, which creates the entry. Your code then proceeds on a fact that stopped being true microseconds ago. No amount of checking closes this, because the check and the create are two separate system calls with a gap between them; the fix has to be a *single* call that both creates and refuses to touch anything that already exists. That is what `tempfile.mkstemp()` does. It calls `os.open` with the exclusive-create flags — create-or-fail rather than create-or-open — and mode `0o600`, so the kernel guarantees that either you created a brand-new file or you got `FileExistsError` and `tempfile` retries with a fresh random name. Exclusive create also refuses to follow a symbolic link, which kills the link-planting attack outright. The name itself is drawn from a strong random source, not a counter, so guessing ahead is not practical either. `tempfile.NamedTemporaryFile`, `TemporaryFile`, `mkdtemp` and `TemporaryDirectory` all sit on the same primitive and inherit the same guarantees; the directory helpers create with mode `0o700`. `tempfile.mktemp()` is the one function in the module that does not: it returns a name and creates nothing, leaving you with exactly the gap described above. It is long-deprecated and still present on Python 3.14, which is why it still turns up in old code and in this question. ## What the platform does and does not give you On Unix the temp directory usually carries the sticky bit, which stops one user deleting or renaming another user's entries. That is worth knowing, but it does not help here: the attack is creating a name *you have not created yet*, which the sticky bit permits. Container and systemd-style private temp namespaces do help, by making the directory unshared — but a library cannot assume its caller deployed it that way, so the code still has to be correct on a shared machine. ## The practical rules - Never assemble a temp path from `tempfile.gettempdir()` plus a name of your own. Let the module name it. - One run, one scratch root: a `TemporaryDirectory` per run gives isolation and single-call cleanup at the same time, and inside it you can use whatever readable filenames you like, because the *directory* is the unguessable part. - Do not compute a path once and share it — whether as a module constant or a default argument evaluated at definition time. Compute it per call. - Never widen the permissions of a temp file back out to `0o644` "so the other step can read it"; move the handoff inside your own directory or pass the descriptor. - If you need the file in a specific directory, pass `dir=` to `mkstemp` rather than building the path — you keep the atomic exclusive create and choose the location. ## The tell in an interview A candidate who says "I check whether the file exists first" has not internalised the race; a candidate who says "I add the process id to the name" has made the name harder to guess but has not made the create atomic, and process ids are both reused and enumerable. The answer that lands is the one that names the gap between check and create, and points at a single atomic call that has no gap.
- A colleague adds the process id to the temp filename and calls it fixed. Is it?No. It reduces accidental collisions between concurrent runs, but process ids are small, enumerable and reused, so an attacker can pre-create every plausible name cheaply. More importantly it leaves the structure untouched: the name is still chosen before the file is created, so the check-then-create gap is still there. The property you need is an atomic exclusive create, not a less guessable string.
- Does the sticky bit on the temp directory make this safe on Unix?It helps with a different attack. The sticky bit stops one user deleting or renaming entries owned by another, so an attacker cannot remove your existing file. It does nothing about creating a name you have not created yet, which is the whole race. Private per-service temp namespaces do remove the exposure, but library code cannot assume its deployment provides one.
- How do you give one run a private scratch area with readable filenames inside it?Create a `tempfile.TemporaryDirectory()` per run and put ordinary, human-readable names inside it — `batch.csv`, `rejects.csv`. The unguessable, exclusively-created part is the directory, created at mode `0o700`, so nothing outside can pre-create entries in it. You get isolation between concurrent runs, readable paths in logs, and cleanup as one operation on exit.
Announcing in advance which locker you will use in a public changing room: by the time you get there, someone may have put their own lock on it, or swapped it for one that opens into your neighbour's.
saying these in an interview costs you the question
- Checks os.path.exists first and calls the race closed
- Adds the process id and considers the name unguessable
- Thinks tempfile.mktemp is merely stylistically discouraged
- Believes the sticky bit prevents a pre-created path
- Chmods a temp file to 0o644 so another step can read it
- Sees only the collision bug and misses the symlink attack