Skip to content

tmpdir: prevent symlink attacks and TOCTOU races (CVE-2025-71176)#14279

Open
laurac8r wants to merge 88 commits intopytest-dev:mainfrom
laurac8r:hotfix/cve
Open

tmpdir: prevent symlink attacks and TOCTOU races (CVE-2025-71176)#14279
laurac8r wants to merge 88 commits intopytest-dev:mainfrom
laurac8r:hotfix/cve

Conversation

@laurac8r
Copy link
Copy Markdown

@laurac8r laurac8r commented Mar 9, 2026

Summary

Replace the predictable pytest-of-<user> rootdir with a randomly-named rootdir created via tempfile.mkdtemp to prevent the entire class of predictable-name attacks (symlink races, DoS via pre-created files/dirs). As defense-in-depth, open the rootdir with os.open using O_NOFOLLOW and O_DIRECTORY flags and perform ownership/permission checks via fd-based fstat/fchmod to eliminate TOCTOU races.

Fixes CVE-2025-71176.

closes #13669

Changes

  • Randomly-named rootdir: Use tempfile.mkdtemp(prefix=f"pytest-of-{user}-", dir=temproot) instead of a fixed pytest-of-{user} directory, making pre-creation attacks infeasible.
  • _safe_open_dir context manager: Opens the rootdir with O_NOFOLLOW | O_DIRECTORY (where available), yields the fd, and guarantees cleanup. Symlinks are rejected at the os.open level.
  • fd-based ownership & permission checks: fstat and fchmod operate on the open file descriptor, eliminating the TOCTOU window between stat/chmod and 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.
  • Comprehensive tests: Symlink rejection, wrong-owner rejection, missing O_NOFOLLOW/O_DIRECTORY fallback, predictable-path DoS immunity, _cleanup_old_rootdirs behavior, _safe_open_dir fd lifecycle, config validation, and session-finish edge cases.

  • Include documentation when adding new features.
  • Include new tests or update existing tests when applicable.
  • Allow maintainers to push and squash when merging my commits. Please uncheck this if you prefer to squash the commits yourself.

If this change fixes an issue, please:

  • Add text like closes #XYZW to the PR description and/or commits (where XYZW is the issue number). See the github docs for more information.

Important

Unsupervised agentic contributions are not accepted. See our AI/LLM-Assisted Contributions Policy.

  • If AI agents were used, they are credited in Co-authored-by commit 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 changelog directory, with a name like <ISSUE NUMBER>.<TYPE>.rst. See changelog/README.rst for details.

    Write sentences in the past or present tense, examples:

    • Improved verbose diff output with sequences.
    • Terminal summary statistics now use multiple colors.

    Also make sure to end the sentence with a ..

  • Add yourself to AUTHORS in alphabetical order.

laurac8r added 3 commits March 9, 2026 00:49
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
@psf-chronographer psf-chronographer bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Mar 9, 2026
@orlitzky
Copy link
Copy Markdown

orlitzky commented Mar 9, 2026

This certainly looks like an improvement, but it's still possible for any user to DoS the machine until a reboot with for x in $(cat /etc/passwd | cut -f1 -d:); do touch /tmp/pytest-of-$x; done;.

laurac8r and others added 6 commits March 10, 2026 22:55
Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <wk.cvs.github@sydorenko.org.ua>
Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <wk.cvs.github@sydorenko.org.ua>
# Conflicts:
#	changelog/13669.bugfix.rst
laurac8r added 22 commits March 28, 2026 14:14
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
hotfix: _check_symlink_attack_safety(path) runs after the path is extended for Windows

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
Copy link
Copy Markdown
Member

@bluetech bluetech left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

@orlitzky orlitzky Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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():
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

@nicoddemus nicoddemus Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

bluetech added a commit to bluetech/pytest that referenced this pull request Mar 31, 2026
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
bluetech added a commit to bluetech/pytest that referenced this pull request Mar 31, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:chronographer:provided (automation) changelog entry is part of PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vulnerable tmpdir handling (CVE-2025-71176)

6 participants