tempfile.gettempdir() picks a directory from the environment — how do you harden a job that trusts it?
answer
- The directory is an inherited input
- Three environment variables, then fallbacks
- Resolved once, then cached
- Pass the directory instead of asking
- Verify owner and mode before writing
basics
~20 stempfile.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 linesimport 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
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.
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=.
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.
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