posted 2026-01-22
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.
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.
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 = 1in /etc/sysctl.conf to mitigate these exploits wholesale.