michael orlitzky

CVE-2025-71176: pytest vulnerable tmpdir handling

posted 2026-01-22

Product
pytest Python testing framework
Versions affected
All modern
Published on
2026-01-22
Bug report
https://github.com/pytest-dev/pytest/issues/13669
CVE
https://www.cve.org/CVERecord?id=CVE-2025-71176
OSS-security
https://www.openwall.com/lists/oss-security/2026/01/21/5

Summary

On UNIX, pytest uses a predictable naming scheme under /tmp with a UID check for added security. The UID check however will follow symlinks, and is vulnerable to TOCTOU. This leaves pytest vulnerable to several well-known vulnerabilities on multi-user systems. The risk ultimately depends on how pytest is used, but denial of service is trivial and code execution is possible.

Details

The temporary directory used by pytest is determined in src/_pytest/tmpdir.py starting around line 155. On UNIX systems, it will use the system's (that is, python's) default of /tmp as the base, and then append the predictable pytest-of-{user} to it:

def getbasetemp(self) -> Path:
    ...
    from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT")
    temproot = Path(from_env or tempfile.gettempdir()).resolve()
    user = get_user() or "unknown"
    # use a sub-directory in the temproot to speed-up
    # make_numbered_dir() call
    rootdir = temproot.joinpath(f"pytest-of-{user}")
    try:
        rootdir.mkdir(mode=0o700, exist_ok=True)

Using a predictable name under /tmp is always a bad idea, but the exist_ok=True here makes it worse, because an attacker can pre-create (and therefore own) the directory that pytest wants to use. Someone was aware of this:

    # Because we use exist_ok=True with a predictable name, make sure
    # we are the owners, to prevent any funny business (on unix, where
    # temproot is usually shared).
    # Also, to keep things private, fixup any world-readable temp
    # rootdir's permissions. Historically 0o755 was used, so we can't
    # just error out on this, at least for a while.
    uid = get_user_id()
    if uid is not None:
        rootdir_stat = rootdir.stat()
        if rootdir_stat.st_uid != uid:
            raise OSError(
                f"The temporary directory {rootdir} is not owned "
                "by the current user. Fix this and try again."
            )
        if (rootdir_stat.st_mode & 0o077) != 0:
            os.chmod(rootdir, rootdir_stat.st_mode & ~0o077)

Unfortunately, this does not fully address the problem. First of all, if someone has pre-created the directory /tmp/pytest-for-mjo, pytest will crash with an error telling me to fix it. But of course I can't fix it, because I don't own the directory—that's the whole point of the UID check. So there is a trivial denial of service available.

Second, the UID check is vulnerable to symlink attacks, because stat() follows symlinks. An attacker can own the symlink /tmp/pytest-for-mjo, so long as it points to a directory owned by mjo for the duration of the check. A relatively benign way to exploit this would be to write junk to some other directory owned by the victim.

Less benign would be to combine it with TOCTOU: there is a window between when the UID and permissions are verified, and when the directory is actually used. While this could conceivably happen during a scheduled wipe of /tmp, it is more likely to be exploited in concert with the symlink issue. A symlink that passes the UID/permission checks is easy to replace (before it is used) with a directory owned by the attacker.

Resolution

The issue is as yet unaddressed.

One easy workaround is to securely create a new temporary directory yourself with mktemp -d, and then use that to override the pytest default. For example,

user $ PYTEST_DEBUG_TEMPROOT=$(mktemp -d) pytest

On Linux, it is also wise to set

fs.protected_fifos = 2
fs.protected_regular = 2
fs.protected_symlinks = 1
fs.protected_hardlinks = 1

in /etc/sysctl.conf to mitigate these exploits wholesale.