Skip to content

Commit 9dc54f7

Browse files
committed
tmpdir: fix temporary directories created with world-readable permissions
(Written for a Unix system, but might be applicable to Windows as well). pytest creates a root temporary directory under /tmp, named `pytest-of-<username>`, and creates tmp_path's and other under it. /tmp is shared between all users of the system. This root temporary directory was created with 0o777&~umask permissions, which usually becomes 0o755, meaning any user in the system could list and read the files, which is undesirable. Use 0o700 permissions instead. Also for subdirectories, because the root dir is adjustable.
1 parent 93dbae2 commit 9dc54f7

File tree

5 files changed

+37
-12
lines changed

5 files changed

+37
-12
lines changed

changelog/8414.bugfix.rst

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pytest used to create directories under ``/tmp`` with world-readable
2+
permissions. This means that any user in the system was able to read
3+
information written by tests in temporary directories (such as those created by
4+
the ``tmp_path``/``tmpdir`` fixture). Now the directories are created with
5+
private permissions.

src/_pytest/pathlib.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -207,15 +207,15 @@ def _force_symlink(
207207
pass
208208

209209

210-
def make_numbered_dir(root: Path, prefix: str) -> Path:
210+
def make_numbered_dir(root: Path, prefix: str, mode: int = 0o700) -> Path:
211211
"""Create a directory with an increased number as suffix for the given prefix."""
212212
for i in range(10):
213213
# try up to 10 times to create the folder
214214
max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1)
215215
new_number = max_existing + 1
216216
new_path = root.joinpath(f"{prefix}{new_number}")
217217
try:
218-
new_path.mkdir()
218+
new_path.mkdir(mode=mode)
219219
except Exception:
220220
pass
221221
else:
@@ -347,13 +347,13 @@ def cleanup_numbered_dir(
347347

348348

349349
def make_numbered_dir_with_cleanup(
350-
root: Path, prefix: str, keep: int, lock_timeout: float
350+
root: Path, prefix: str, keep: int, lock_timeout: float, mode: int,
351351
) -> Path:
352352
"""Create a numbered dir with a cleanup lock and remove old ones."""
353353
e = None
354354
for i in range(10):
355355
try:
356-
p = make_numbered_dir(root, prefix)
356+
p = make_numbered_dir(root, prefix, mode)
357357
lock_path = create_cleanup_lock(p)
358358
register_cleanup_lock_removal(lock_path)
359359
except Exception as exc:

src/_pytest/pytester.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1426,7 +1426,7 @@ def runpytest_subprocess(self, *args, timeout: Optional[float] = None) -> RunRes
14261426
:rtype: RunResult
14271427
"""
14281428
__tracebackhide__ = True
1429-
p = make_numbered_dir(root=self.path, prefix="runpytest-")
1429+
p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700)
14301430
args = ("--basetemp=%s" % p,) + args
14311431
plugins = [x for x in self.plugins if isinstance(x, str)]
14321432
if plugins:
@@ -1445,7 +1445,7 @@ def spawn_pytest(
14451445
The pexpect child is returned.
14461446
"""
14471447
basetemp = self.path / "temp-pexpect"
1448-
basetemp.mkdir()
1448+
basetemp.mkdir(mode=0o700)
14491449
invoke = " ".join(map(str, self._getpytestargs()))
14501450
cmd = f"{invoke} --basetemp={basetemp} {string}"
14511451
return self.spawn(cmd, expect_timeout=expect_timeout)

src/_pytest/tmpdir.py

+10-6
Original file line numberDiff line numberDiff line change
@@ -90,22 +90,22 @@ def mktemp(self, basename: str, numbered: bool = True) -> Path:
9090
basename = self._ensure_relative_to_basetemp(basename)
9191
if not numbered:
9292
p = self.getbasetemp().joinpath(basename)
93-
p.mkdir()
93+
p.mkdir(mode=0o700)
9494
else:
95-
p = make_numbered_dir(root=self.getbasetemp(), prefix=basename)
95+
p = make_numbered_dir(root=self.getbasetemp(), prefix=basename, mode=0o700)
9696
self._trace("mktemp", p)
9797
return p
9898

9999
def getbasetemp(self) -> Path:
100-
"""Return base temporary directory."""
100+
"""Return the base temporary directory, creating it if needed."""
101101
if self._basetemp is not None:
102102
return self._basetemp
103103

104104
if self._given_basetemp is not None:
105105
basetemp = self._given_basetemp
106106
if basetemp.exists():
107107
rm_rf(basetemp)
108-
basetemp.mkdir()
108+
basetemp.mkdir(mode=0o700)
109109
basetemp = basetemp.resolve()
110110
else:
111111
from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT")
@@ -114,9 +114,13 @@ def getbasetemp(self) -> Path:
114114
# use a sub-directory in the temproot to speed-up
115115
# make_numbered_dir() call
116116
rootdir = temproot.joinpath(f"pytest-of-{user}")
117-
rootdir.mkdir(exist_ok=True)
117+
rootdir.mkdir(mode=0o700, exist_ok=True)
118118
basetemp = make_numbered_dir_with_cleanup(
119-
prefix="pytest-", root=rootdir, keep=3, lock_timeout=LOCK_TIMEOUT
119+
prefix="pytest-",
120+
root=rootdir,
121+
keep=3,
122+
lock_timeout=LOCK_TIMEOUT,
123+
mode=0o700,
120124
)
121125
assert basetemp is not None, basetemp
122126
self._basetemp = basetemp

testing/test_tmpdir.py

+16
Original file line numberDiff line numberDiff line change
@@ -445,3 +445,19 @@ def test(tmp_path):
445445
# running a second time and ensure we don't crash
446446
result = pytester.runpytest("--basetemp=tmp")
447447
assert result.ret == 0
448+
449+
450+
@pytest.mark.skipif(not hasattr(os, "getuid"), reason="checks unix permissions")
451+
def test_tmp_path_factory_create_directory_with_safe_permissions(
452+
tmp_path: Path, monkeypatch,
453+
) -> None:
454+
"""Verify that pytest creates directories under /tmp with private permissions."""
455+
# Use the test's tmp_path as the system temproot (/tmp).
456+
monkeypatch.setenv("PYTEST_DEBUG_TEMPROOT", str(tmp_path))
457+
tmp_factory = TempPathFactory(None, lambda *args: None, _ispytest=True)
458+
basetemp = tmp_factory.getbasetemp()
459+
460+
# No world-readable permissions.
461+
assert (basetemp.stat().st_mode & 0o077) == 0
462+
# Parent too (pytest-of-foo).
463+
assert (basetemp.parent.stat().st_mode & 0o077) == 0

0 commit comments

Comments
 (0)