skip to content

tempfile.gettempdir() picks a directory from the environment — how do you harden a job that trusts it?

level: seniorimportance: should knowfreq 30%

answer

  1. The directory is an inherited input
  2. Three environment variables, then fallbacks
  3. Resolved once, then cached
  4. Pass the directory instead of asking
  5. Verify owner and mode before writing

basics

~20 s

tempfile.gettempdir() honours TMPDIR, TEMP and TMP from the inherited environment before falling back to /tmp. For sensitive staging, pass an explicit dir= you created yourself with mode 0o700 rather than trusting whatever the caller exported.

solid answer

~40 s

`tempfile.gettempdir()` is not a constant. On POSIX it consults the environment variables `TMPDIR`, `TEMP` and `TMP` in order, then falls back to a list of standard directories and finally the current working directory, keeping the first one it can actually create a file in. The result is cached in `tempfile.tempdir` for the life of the process. Every one of those inputs is chosen by whoever launched you — a shell, a scheduler, a container image — so the location of your staging data is an inherited, attacker-influenceable input. Harden it by not asking: create a directory you own with `os.makedirs(path, mode=0o700)`, verify its owner and mode with `os.stat`, and pass it as `dir=` to `tempfile.mkstemp` or `tempfile.NamedTemporaryFile`. Put it on the same filesystem as the final destination so the promotion can be an `os.replace`.

code

python · 14 lines
python
import os
import stat
import tempfile

staging = "/tmp/etl-staging"
os.makedirs(staging, mode=0o700, exist_ok=True)
info = os.stat(staging)
if info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) & 0o077:
    raise PermissionError(f"unsafe staging directory: {staging}")

with tempfile.TemporaryDirectory(dir=staging) as run_dir:
    fd, path = tempfile.mkstemp(dir=run_dir, suffix=".csv")
    os.close(fd)
    print(path)

go deeper

for a junior

Recall that the temp directory is looked up, not fixed: TMPDIR and friends decide it, with /tmp as the usual fallback. Know that tempfile.mkstemp accepts a dir= argument.

for a middle

Explain the resolution order and the caching in tempfile.tempdir, and why an inherited environment variable counts as untrusted input. Show creating a directory with mode 0o700 and passing it as dir=.

for a senior

Demonstrate the operational reasoning: verify owner and mode at startup, fail loudly on a bad directory, keep staging on the destination filesystem so promotion is a single rename, and think about whether the mount is tmpfs or disk.

for a principal

Own where staging data is allowed to live across the estate: which classes of payload may touch local disk, whether shared temp directories are permitted for services, and how that is expressed in deployment units rather than rediscovered per job.

### gettempdir is a lookup, not a constant `tempfile.gettempdir()` resolves a directory the first time anything in the module needs one, and caches it in the module-level `tempfile.tempdir`. On POSIX the search order is: the environment variables `TMPDIR`, `TEMP` and `TMP`, in that order; then a fixed list of conventional directories (`/tmp`, `/var/tmp`, `/usr/tmp`); then the current working directory. Each candidate is tested by actually creating and removing a file in it, so an unwritable candidate is skipped rather than returned. `tempfile.gettempdirb()` is the same value as bytes. Two properties of that algorithm matter for a service. **The first three inputs are inherited.** Environment variables come from whoever started the process — an interactive shell, a scheduler, a supervisor unit, an image's `ENV`, or a parent process that itself inherited them. If any of those is under an attacker's influence, so is the directory your secrets land in. The attack is not exotic: redirect `TMPDIR` at a directory the attacker owns and every staging file the job writes lands somewhere they can read, and where they can pre-create names to trip your own error handling. **The fallback to the current working directory is worse than it looks.** If the conventional directories are unwritable — a read-only root, a hardened container — the module can land on `os.getcwd()`, which for a service is often the deployment directory. Staging data then appears next to code, gets picked up by a backup or an image build, and is quietly persisted long after the job ended. ### The concrete failure Take a nightly export that stages warehouse extracts before promoting them. Its workers each write a partitioned extract to a temp file, then move it into place. The team notices the run reporting an 83% cache-hit rate against the source system yet producing an extract with duplicated rows, and the artefact is a clock-skew one: two workers on hosts whose clocks differ derive the same staging filename from a wall-clock timestamp, and because the shared `TMPDIR` on that host class points at a directory both can write, they collide there rather than in isolation. Every part of that is a directory-trust failure before it is a naming failure. Had each worker created its own `0o700` directory and passed it as `dir=`, the same colliding name would have been two different files in two different directories, and the unpredictable component `tempfile` adds would have prevented the collision anyway. ### Hardening, in order 1. **Stop asking.** Choose the directory in configuration, not from the environment: create it with `os.makedirs(staging_dir, mode=0o700, exist_ok=True)` and pass `dir=staging_dir` to `tempfile.mkstemp`, `tempfile.NamedTemporaryFile` or `tempfile.TemporaryDirectory`. 2. **Verify what you got.** `exist_ok=True` will happily accept a directory somebody else created. `os.stat` the path and confirm the owner is your uid and the mode has no group or other bits before writing anything into it. 3. **Prefer a per-run private directory.** `tempfile.mkdtemp(dir=staging_dir)` creates a `0o700` directory; inside a directory no other account may enter, individual filenames stop being a security question at all. `tempfile.TemporaryDirectory` wraps that with cleanup. 4. **Keep it on the destination's filesystem.** Promotion should be `os.replace`, which is atomic only within one filesystem and raises `OSError` across a boundary. A `TMPDIR` that lands on a different mount silently converts your atomic rename into a copy — with a partially visible destination file. 5. **Control the environment you hand to children.** A child process resolves its own temp directory from its own environment, so an explicit environment for children is part of the same control. 6. **Know what the mount actually is.** A `tmpfs` temp directory lives in RAM and disappears on reboot, which is good for secrets and bad for large extracts; a disk-backed one persists across a crash, which is the reverse trade. ### What to say about it in review The reviewable rule is short: a library may use `tempfile.gettempdir()` because its caller decides policy, but a *service* should name its staging directory explicitly and verify it. Anything sensitive gets a private directory rather than a shared one, and the promotion step is a rename within a filesystem you chose.

  • What should the job do if TMPDIR points at a directory it does not own?
    Refuse to use it. `os.stat` the candidate, compare `st_uid` against `os.getuid()`, and reject any mode with group or other bits set. For a service the right response is a startup failure with a clear message, not a fallback — silently writing elsewhere hides the misconfiguration and may put the data somewhere worse.
  • Why does the staging file need to be on the same filesystem as its final destination?
    Because `os.replace` is atomic only within a filesystem. Across a mount boundary it raises `OSError`, and the usual workaround is a copy followed by a delete — during which the destination is visible half-written. Creating the temp file with `dir=` set to the destination's own directory keeps the promotion a single rename.
  • Does calling tempfile.gettempdir() twice re-read TMPDIR?
    No. The resolved value is cached in the module-level `tempfile.tempdir` on first use and reused for the life of the process, so mutating `os.environ['TMPDIR']` after any part of the module has already run has no effect. If you need to override it deliberately, set `tempfile.tempdir` yourself, or better, pass `dir=` at each call site.

saying these in an interview costs you the question

  • Assumes gettempdir always returns /tmp
  • Treats environment variables as trusted configuration
  • Sets TMPDIR at runtime and expects it to take effect
  • Never checks the owner or mode of the staging directory
  • Stages across a mount boundary and still calls the rename atomic

context