tmpdir: prevent symlink attacks and TOCTOU races (CVE-2025-71176)#14279
tmpdir: prevent symlink attacks and TOCTOU races (CVE-2025-71176)#14279laurac8r wants to merge 88 commits intopytest-dev:mainfrom
Conversation
Open the base temporary directory using `os.open` with `O_NOFOLLOW` and `O_DIRECTORY` flags to prevent symlink attacks. Use the resulting file descriptor for `fstat` and `fchmod` operations to eliminate Time-of-Check Time-of-Use (TOCTOU) races. Co-authored-by: Windsurf, Gemini
Co-authored-by: Windsurf, Gemini
Co-authored-by Windsurf
Co-authored-by Windsurf, Gemini
for more information, see https://pre-commit.ci
Co-authored-by: Windsurf
for more information, see https://pre-commit.ci
Co-authored-by: Windsurf
|
This certainly looks like an improvement, but it's still possible for any user to DoS the machine until a reboot with |
Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <wk.cvs.github@sydorenko.org.ua>
Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <wk.cvs.github@sydorenko.org.ua>
Co-authored-by: Windsurf
# Conflicts: # changelog/13669.bugfix.rst
Co-authored-by: Claude
Co-authored-by: Claude
Co-authored-by: Claude
Co-authored-by: Claude
Co-authored-by: Claude
…ime helper that returns inf on failure, so one bad entry no longer aborts all cleanup Co-authored-by: Claude
…ndles POSIX, os.chmod falls back for Windows only Co-authored-by: Claude
…ety; rm_rf passes stacklevel=2, safe_rmtree uses default 3 Co-authored-by: Claude
…p_and_tighten_permissions docstring Co-authored-by: Claude
…fining directory enumeration and error handling
Co-authored-by: Claude
bluetech
left a comment
There was a problem hiding this comment.
Not a proper review, but some comments from a quick read.
| # to verify *path* itself is not a symlink (done above). | ||
| # When it is False a TOCTOU window remains for contents *inside* the tree, | ||
| # so we emit a one-time warning. | ||
| if not shutil.rmtree.avoids_symlink_attacks: |
There was a problem hiding this comment.
I don't think that just because some poor soul happens to be using pytest on a system where avoids_symlink_attacks is false, we should warn them every time. There's nothing they can do about it after all.
There was a problem hiding this comment.
I think all recent (>= POSIX 2008) platforms should be safe, so maybe it is a better idea to bail out here rather than run an unsafe recursive delete on the user's behalf? (In other words, let them do it, so pytest can't be blamed.) With the understanding that this will rarely happen on real systems.
|
|
||
| Raises ``OSError`` if *path* is a symlink. | ||
| """ | ||
| if path.is_symlink(): |
There was a problem hiding this comment.
For me os.shutil already refuses a symlink argument. Is this check needed on our side then?
| # platform, fall back to a safe prefix. | ||
| rootdir_prefix = "pytest-of-unknown-" | ||
| rootdir = Path(tempfile.mkdtemp(prefix=rootdir_prefix, dir=temproot)) | ||
| # Defense-in-depth: verify ownership and tighten permissions |
There was a problem hiding this comment.
Seems excessive to me. mkdtemp documentation already says "Creates a temporary directory in the most secure manner possible", why do we need to double check it?
| keep=keep, | ||
| lock_timeout=LOCK_TIMEOUT, | ||
| mode=0o700, | ||
| _cleanup_old_rootdirs( |
There was a problem hiding this comment.
The previous code comment said "use a sub-directory in the temproot to speed-up make_numbered_dir() call", and that's relevant here -- now this call will do a full scandir of /tmp on every cleanup, and the problem is that /tmp can accumulate a bunch of garbage via various programs, and seems like the previous code tried to avoid scanning it by using a sub-directory.
There was a problem hiding this comment.
Good catch. 👍
This PR now creates this structure:
/tmp/
pytest-of-user-kjnasjnd/
pytest-of-user-deadbeef/
pytest-of-user-basu1234/
pytest-of-user-olsdanj1/
If instead it created this:
/tmp/
pytest-of-user/
kjnasjnd/
deadbeef/
basu1234/
olsdanj1/
Would that still work to prevent the security concerns this PR addresses?
I'm guessing the fact that an attack can create files in /tmp/pytest-of-user would still be a problem?
…tack_safety in rm_rf function
…y checks for avoids_symlink_attacks
for more information, see https://pre-commit.ci
A previous fix for insecure temporary directory issue c49100c wasn't sufficient because it followed symlinks. Stop following symlinks, and reject if a symlink; we know it shouldn't be. Fix pytest-dev#14279. [0] https://www.openwall.com/lists/oss-security/2026/01/21/5
A previous fix for insecure temporary directory issue c49100c wasn't sufficient because it followed symlinks. Stop following symlinks, and reject if a symlink; we know it shouldn't be. Fix pytest-dev#14279. [0] https://www.openwall.com/lists/oss-security/2026/01/21/5
Summary
Replace the predictable
pytest-of-<user>rootdir with a randomly-named rootdir created viatempfile.mkdtempto prevent the entire class of predictable-name attacks (symlink races, DoS via pre-created files/dirs). As defense-in-depth, open the rootdir withos.openusingO_NOFOLLOWandO_DIRECTORYflags and perform ownership/permission checks via fd-basedfstat/fchmodto eliminate TOCTOU races.Fixes CVE-2025-71176.
closes #13669
Changes
tempfile.mkdtemp(prefix=f"pytest-of-{user}-", dir=temproot)instead of a fixedpytest-of-{user}directory, making pre-creation attacks infeasible._safe_open_dircontext manager: Opens the rootdir withO_NOFOLLOW | O_DIRECTORY(where available), yields the fd, and guarantees cleanup. Symlinks are rejected at theos.openlevel.fstatandfchmodoperate on the open file descriptor, eliminating the TOCTOU window betweenstat/chmodand the actual directory._cleanup_old_rootdirs: Garbage-collects stale randomly-named rootdirs from previous sessions, respecting the retention count. The current session's rootdir is never removed.O_NOFOLLOW/O_DIRECTORYfallback, predictable-path DoS immunity,_cleanup_old_rootdirsbehavior,_safe_open_dirfd lifecycle, config validation, and session-finish edge cases.If this change fixes an issue, please:
closes #XYZWto the PR description and/or commits (whereXYZWis the issue number). See the github docs for more information.Important
Unsupervised agentic contributions are not accepted. See our AI/LLM-Assisted Contributions Policy.
Co-authored-bycommit trailers.Unless your change is trivial or a small documentation fix (e.g., a typo or reword of a small section) please:
Create a new changelog file in the
changelogdirectory, with a name like<ISSUE NUMBER>.<TYPE>.rst. See changelog/README.rst for details.Write sentences in the past or present tense, examples:
Also make sure to end the sentence with a
..Add yourself to
AUTHORSin alphabetical order.