Skip to content

Commit 1278f8b

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 0dd1e5b commit 1278f8b

File tree

5 files changed

+42
-13
lines changed

5 files changed

+42
-13
lines changed

changelog/8414.bugfix.rst

Lines changed: 5 additions & 0 deletions
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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -205,15 +205,15 @@ def _force_symlink(
205205
pass
206206

207207

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

346346

347347
def make_numbered_dir_with_cleanup(
348-
root: Path, prefix: str, keep: int, lock_timeout: float
348+
root: Path,
349+
prefix: str,
350+
keep: int,
351+
lock_timeout: float,
352+
mode: int,
349353
) -> Path:
350354
"""Create a numbered dir with a cleanup lock and remove old ones."""
351355
e = None
352356
for i in range(10):
353357
try:
354-
p = make_numbered_dir(root, prefix)
358+
p = make_numbered_dir(root, prefix, mode)
355359
lock_path = create_cleanup_lock(p)
356360
register_cleanup_lock_removal(lock_path)
357361
except Exception as exc:

src/_pytest/pytester.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1456,7 +1456,7 @@ def runpytest_subprocess(
14561456
:py:class:`Pytester.TimeoutExpired`.
14571457
"""
14581458
__tracebackhide__ = True
1459-
p = make_numbered_dir(root=self.path, prefix="runpytest-")
1459+
p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700)
14601460
args = ("--basetemp=%s" % p,) + args
14611461
plugins = [x for x in self.plugins if isinstance(x, str)]
14621462
if plugins:
@@ -1475,7 +1475,7 @@ def spawn_pytest(
14751475
The pexpect child is returned.
14761476
"""
14771477
basetemp = self.path / "temp-pexpect"
1478-
basetemp.mkdir()
1478+
basetemp.mkdir(mode=0o700)
14791479
invoke = " ".join(map(str, self._getpytestargs()))
14801480
cmd = f"{invoke} --basetemp={basetemp} {string}"
14811481
return self.spawn(cmd, expect_timeout=expect_timeout)

src/_pytest/tmpdir.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,22 +94,22 @@ def mktemp(self, basename: str, numbered: bool = True) -> Path:
9494
basename = self._ensure_relative_to_basetemp(basename)
9595
if not numbered:
9696
p = self.getbasetemp().joinpath(basename)
97-
p.mkdir()
97+
p.mkdir(mode=0o700)
9898
else:
99-
p = make_numbered_dir(root=self.getbasetemp(), prefix=basename)
99+
p = make_numbered_dir(root=self.getbasetemp(), prefix=basename, mode=0o700)
100100
self._trace("mktemp", p)
101101
return p
102102

103103
def getbasetemp(self) -> Path:
104-
"""Return base temporary directory."""
104+
"""Return the base temporary directory, creating it if needed."""
105105
if self._basetemp is not None:
106106
return self._basetemp
107107

108108
if self._given_basetemp is not None:
109109
basetemp = self._given_basetemp
110110
if basetemp.exists():
111111
rm_rf(basetemp)
112-
basetemp.mkdir()
112+
basetemp.mkdir(mode=0o700)
113113
basetemp = basetemp.resolve()
114114
else:
115115
from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT")
@@ -119,13 +119,17 @@ def getbasetemp(self) -> Path:
119119
# make_numbered_dir() call
120120
rootdir = temproot.joinpath(f"pytest-of-{user}")
121121
try:
122-
rootdir.mkdir(exist_ok=True)
122+
rootdir.mkdir(mode=0o700, exist_ok=True)
123123
except OSError:
124124
# getuser() likely returned illegal characters for the platform, use unknown back off mechanism
125125
rootdir = temproot.joinpath("pytest-of-unknown")
126-
rootdir.mkdir(exist_ok=True)
126+
rootdir.mkdir(mode=0o700, exist_ok=True)
127127
basetemp = make_numbered_dir_with_cleanup(
128-
prefix="pytest-", root=rootdir, keep=3, lock_timeout=LOCK_TIMEOUT
128+
prefix="pytest-",
129+
root=rootdir,
130+
keep=3,
131+
lock_timeout=LOCK_TIMEOUT,
132+
mode=0o700,
129133
)
130134
assert basetemp is not None, basetemp
131135
self._basetemp = basetemp

testing/test_tmpdir.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,3 +454,19 @@ def test_tmp_path_factory_handles_invalid_dir_characters(
454454
monkeypatch.setattr(tmp_path_factory, "_given_basetemp", None)
455455
p = tmp_path_factory.getbasetemp()
456456
assert "pytest-of-unknown" in str(p)
457+
458+
459+
@pytest.mark.skipif(not hasattr(os, "getuid"), reason="checks unix permissions")
460+
def test_tmp_path_factory_create_directory_with_safe_permissions(
461+
tmp_path: Path, monkeypatch: MonkeyPatch
462+
) -> None:
463+
"""Verify that pytest creates directories under /tmp with private permissions."""
464+
# Use the test's tmp_path as the system temproot (/tmp).
465+
monkeypatch.setenv("PYTEST_DEBUG_TEMPROOT", str(tmp_path))
466+
tmp_factory = TempPathFactory(None, lambda *args: None, _ispytest=True)
467+
basetemp = tmp_factory.getbasetemp()
468+
469+
# No world-readable permissions.
470+
assert (basetemp.stat().st_mode & 0o077) == 0
471+
# Parent too (pytest-of-foo).
472+
assert (basetemp.parent.stat().st_mode & 0o077) == 0

0 commit comments

Comments
 (0)