Repository navigation
Vulnerable tmpdir handling (CVE-2025-71176) #13669
Description
Activity
The predictable base is a intentional feature
Thanks for the heads up on the feature
Reacted by Nir SofferIf the predictable base is just for convenience (to make them easy to find/identify), it might be acceptable to pass it as a
prefix=to the python tempdir functions. That would result in a directory likepytest-for-<user>.XXXXXX, whereXXXXXXis a random number.we use the
pytest-for-$USERas base-tmp for more, have to decide what to do with it as its practically a breaking change in uxmy initial impression is that id prefer to harden obtaining that directory - but i'm unfamiliar with the attack surface atm
Looking at the list of weird things that
mkstemp()does is a good place to start, but it really depends on how pedantic you want to be. The fundamental issue is that it's not safe to do anything in a directory that is writable by someone you don't trust completely. Usually that's only yourself and root. Right away this raises some questions:- Do we assume that if
PYTEST_DEBUG_TEMPROOTis taken from the environment, that it's completely safe (i.e. that it meets those writability requirements)? - If we use the default
TMPDIRon UNIX, what assumptions do we want to make on its permissions? Usually/tmpis world writable, which is already bad, but maybe e.g. some other users are allowed to bind-mount directories there?
In the "normal" case where we're using
/tmpand it's world-writable but there's no other funny business, you have symlinks, hardlinks, and TOCTOU to worry about. Symlinks can be avoided withlstat(), but there's a risk that the path you're lstat'ing will be replaced between the time that you lstat it and the time that you use it. There's also a chance that some directory higher up is a symlink;lstat()only checks the very last path component. Hardlinks to directories aren't allowed on linux, but that's actually not part of the POSIX standard. If you want to be safe on systems where hardlinks to directories are allowed, you have all of the same issues you do with symlinks, but now with hardlinks. Actually it's worse because "is this path a hardlink?" is a meaningless question; you have to do astat()and count the number of hardlinks, but that number can change behind your back -- there's no way to do it atomically and fail. (Bind mounts are basically hard links to directories in this context.)You can make it very hard to exploit by doing a lot of checks, but it will never be 100% safe from every contrived scenario. Even using the python library functions to obtain a directory "securely" is probably vulnerable to edge cases. But the benefit of the latter would be that it is no longer your problem :)
- Do we assume that if
Oh, and suppose we manage to make the permissions/ownership check bullet-proof. Someone else can create
pytest-for-orlitzkywhen the machine reboots or when/tmpis cleaned. The next time I try to run pytest, it will raise anOSErrortelling me to fix the ownership issue. But I can't fix it! If I could, the permissions check wouldn't be worth anything.Let's keep in mind that pytest is not commonly a service on a multi user system but rather a test tool you run in isolated ci and/or from the primary user
We don't have the same attack exposure as a unix service on a multi user system
Reacted by Pierre Sassoulas and Nir SofferThat being said we should manage the attack surface and/or ensure user are aware of hardening steps to take
- addedstatus: help wanteddevelopers would like help from experts on this topicdevelopers would like help from experts on this topicplugin: tmpdirrelated to the tmpdir builtin pluginrelated to the tmpdir builtin plugin
on Sep 7, 2025 Hi there :)
Thanks for bringing this up — this is indeed a legitimate security concern.
The current implementation in
tmpdir.pycreates a predictable temporary directory name based on the username (pytest-of-{user}), and then verifies ownership withstat(). This design is vulnerable to symlink and TOCTOU (Time-Of-Check to Time-Of-Use) attacks. An attacker could pre-create a symlink/tmp/pytest-of-youruserpointing to a different location they control, and pytest would end up writing to that location. Changing the symlink between the ownership check and actual directory use is a classic TOCTOU race condition.Even though Linux provides protections via
fs.protected_hardlinksandfs.protected_symlinks, these are not POSIX compliant and not enabled by default. This means such vulnerabilities can still exist in many environments.A more secure approach would be:
- Avoid predictable directory names for temporary files.
- Use Python’s built-in
tempfile.TemporaryDirectory()ortempfile.mkdtemp()to generate secure, random directories. - Avoid manual ownership checks and let the OS/tempfile APIs handle security.
This could significantly improve the security of pytest’s tmpdir handling, and I recommend documenting or addressing this in the upstream code.
Thanks for considering this security improvement.
(Hope it helps)
Reacted by Matěj Cepl, Bill Cleveland and Etienne LanduréReacted by Rene Leonhardt- changed the title
[-]Vulnerable tmpdir handling[/-][+]Vulnerable tmpdir handling (CVE-2025-71176)[/+]on Jan 22, 2026 How is this predictable base being used in UX today?
Is it not enough to just print the randomly created tmpdir?
With a standard random suffix like Michael recommended, navigating to it would be extremely easy for users.# Navigate to latest pytest results cd "$(ls -td1 $TMPDIR/pytest-of-$USER.* 2>/dev/null | head -1)"
After investigating, the user could optionally clean-up before the next run if he wants to.
# Remove latest pytest results d="$(ls -td1 $TMPDIR/pytest-of-$USER.* 2>/dev/null | head -1)" && [[ -d "$d" ]] && read -p "Delete last '$d'? (y/N): " r && [[ $r =~ ^[Yy]$ ]] && echo rm -r "$d" && echo "Deleted last '$d'" || echo "Aborting, nothing deleted."
BenjaminBrienen-CanonPP commented
on Jan 30, 2026 More actionsIs there a patched version available?
Reacted by 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко), Freya Bruhin and AdrianHow is this predictable base being used in UX today?
Is it not enough to just print the randomly created tmpdir?Following pytest runs remove older directories based on how many it's set up to preserve. There needs to be a way to figure out which directories are from the last N runs, which wouldn't be possible without predictability.
FWIW, it doesn't seem reasonable to randomize the directory names since it'd break the fundamental features. I imagine one could set the tmp dir env var to something random/they manage at the cost of confusing the cleanup logic.Is there a patched version available?
The easiest workaround is to create a temporary directory securely yourself, and then use that as the pytest root. For example,
PYTEST_DEBUG_TEMPROOT=$(mktemp -d) pytestThis works even if you need the "previous N runs" feature. Just store the directory name returned by
mktemp -dsomewhere safe (environment variable, predictable filename in a directory writable only by you, etc.)... it's not a secret.On linux, setting these sysctls eliminates most of the risk:
fs.protected_fifos = 2 fs.protected_regular = 2 fs.protected_symlinks = 1 fs.protected_hardlinks = 125 remaining items
@orlitzky The CVE mention here: GHSA-6w46-j5rx-g56g link to this issue and claim that the issue is fixed in pytest >= 9.0.3.
If this is not fixed the CVE should be updated, and this issue should not include the CVE in the title.
Reacted by Rene LeonhardtIf this is not fixed the CVE should be updated, and this issue should not include the CVE in the title.
This is the issue that the CVE describes, but some bozo putting "we don't care" in a comment does not make it fixed. I wasted hours explaining all of this, and spent hours reviewing the PR by @laurac8r who took the time to fix everything carefully and correctly, just for it to be thrown away at the last second for a half-assed heuristic that ignores the months of discussion.
The level of incompetence on display in the handling of this issue has been staggering. I will not be wasting any more time on it.
I understand the frustration here, and I want to acknowledge that the concerns raised are valid — the work done on PR #14279 by @laurac8r and the thorough review by @orlitzky are genuinely appreciated.
However, I want to be clear: accusing the maintainers of "incompetence" is unfair and not acceptable. People can reasonably disagree on risk tolerance and tradeoffs without being incompetent. This project is maintained by volunteers making judgment calls under real constraints.
Here's where things stand:
Aspect Merged fix (#14343) Proposed fix (#14279) Symlink attack Fixed Fixed TOCTOU race Partially mitigated Fully eliminated (fd-based) DoS via pre-creation Not fixed (acknowledged in code) Fixed Complexity ~30 lines changed ~580 additions Breaking changes None Yes (path format changes) The tradeoff that was made: the simpler fix closes the specific symlink attack vector with no behavioral changes, at the cost of leaving the DoS and a theoretical TOCTOU window open. The comprehensive fix eliminates the vulnerability class entirely, but introduces breaking changes in path naming and significant complexity around cleanup of randomly-named dirs.
Whether the advisory should say "fixed" is a fair question — the specific PoC no longer works, but the underlying anti-pattern (predictable names in shared
/tmp) remains. I think it's reasonable to say the CVE as described is addressed, while acknowledging that a more robust solution exists and may be worth revisiting in a future major release where breaking changes are acceptable.Reacted by Nir Soffer and abcdenis@RonnyPfannschmidt Thanks for the status update.
- What is the TOCTOU in this case? Regardless of the directory name it can be discovered after we created it, so there is no difference between random name and well known name.
- What is the Dos? another user creating the director for another user preventing the other user from running pytest?
The best way to avoid all issues is to switch to XDG_RUNTIME_DIR. It is owned by the current user and we can use a well know name (e.g /run/user//pytest). When XDG_RUNTIME_DIR is not available it can be emulated with similar problems to the current way.
Hey y'all, if there is an interest, I can revisit this issue and look to bridge the gap between @bluetech 's and my solutions (merged vs proposed), incorporating some of the ideas posited since our PRs were finalized.
Reacted by Rene LeonhardtI think the topic warrants some discussions
Lets not jump into implementation right off the bat as i think there's a need for some bikeshedding
This may be a topic for the pytest sprint
Reacted by Laura KaminskiyTBH not a cybersecurity expert, so I threw the problem at Claude Fable 5. Here's the synopsis:
Four approaches:
- A · Harden in place — fd-based verification (O_NOFOLLOW|O_DIRECTORY + fstat/fchmod), and the bridging idea: on squat detection, warn and fall back to a mkdtemp-random rootdir instead of hard-failing. Converts the DoS into a degraded-but-working run; zero visible change otherwise; shippable in a 9.x minor.
- B · Flat random per session (tmpdir: prevent symlink attacks and TOCTOU races (CVE-2025-71176) #14279's shape) — strongest single mechanism, but carries exactly the costs that killed it: documented-template break, pytest-current gone, temproot-scan GC (bluetech's objection generate-tests aware tmdir breaks if test id contains the path separator #4).
- C · Nested random — honestly assessed as low marginal security over A: inside a verified 0700 parent an attacker can't write, and the parent squat remains. It buys concurrency robustness, not attacker resistance.
- D · Relocate the base — per-platform private dirs (Linux XDG chain; macOS/Windows already private). Dissolves the problem: predictable names become safe, so numbered dirs, retention, and pytest-current all survive.
- E · --tmpdirs list|gc|latest + registry — the enabling layer, answering Ronny's "list/gc command" condition and webknjaz's retention concern verbatim (go clean -cache is the prior-art pattern).
Recommendation — stage it: Phase 1 (minor): A + E groundwork, each line answering a named maintainer objection without touching the documented template. Phase 2 (major): D by default with B+E as the no-private-base fallback, plus the deprecation cycle.
Reacted by Jane DoeReacted by Rene LeonhardtTBH not a cyber...
Four approaches: ...
Here's a quick visual aid that explores the solution space for this #13669 issue: https://laurac8r.github.io/pytest/
Reacted by Nir SofferHere's a quick visual aid that explores the solution space for this #13669 issue: https://laurac8r.github.io/pytest/
Nice summary, but it will be more useful here in markdown format.
@nirs which part?
@nirs which part?
All parts seem good content for this issue or other documents in this project.
Or maybe extract new sub issues for specific problems. Keeping huge issues open for long time is not very useful and makes it hard to understand the status.
+1 for breaking down the problem into sub-issues
Status note: I'm preparing more comprehensive steps on this and am currently researching the fallout of the different options to pin down the tradeoffs across local development, remote/shared machines and CI runners before splitting it into concrete follow-up issues.
The research input so far is at https://github.com/RonnyPfannschmidt-Attic/pytest-tmpdir-13669-research: a squat/symlink scenario matrix run against the candidate rootdir strategies with two real unprivileged users, a retention/GC demo driven by pytest's own cleanup code, and planning notes. Those scripts, outputs and notes were generated and run by Claude (Fable 5.1 via Claude Code) at my prompting; I've read them and am posting them as input, not as decisions.
- added a commit that references this issue
on Sep 16, 2026
Starting around https://github.com/pytest-dev/pytest/blob/main/src/_pytest/tmpdir.py#L156, there are some heuristics to make sure that the tmpdir is safe:
This is vulnerable to symlink attacks, and probably TOCTOU attacks (see e.g. https://owasp.org/www-community/vulnerabilities/Insecure_Temporary_File). The linux kernel has protections for some of these in the
fs.protected_hardlinksandfs.protected_symlinkssysctls, but they violate POSIX and are not enabled by default (though most distros enable them). In any case, they are linux-only.For a proof of concept, create a symlink as some other user from
/tmp/pytest-of-youruserto a directory you own (this will requirefs.protected_symlinksto be disabled). Run pytest as yourself, and it will start writing junk to the directory that the attacker chose. It will also change the permissions on the directory he chose.Obviously whoever wrote this was aware of these issues, but maybe not of how hard they can be to fix completely. This can be made a little bit better by telling
stat()not to follow symlinks, but I would instead recommend getting a random base directory in a secure way (using the python tempdir functions) rather than using a predictable name.There are just too many ways it can go wrong.