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.
This commit is contained in:
parent
93dbae24e1
commit
9dc54f79b0
|
@ -0,0 +1,5 @@
|
||||||
|
pytest used to create directories under ``/tmp`` with world-readable
|
||||||
|
permissions. This means that any user in the system was able to read
|
||||||
|
information written by tests in temporary directories (such as those created by
|
||||||
|
the ``tmp_path``/``tmpdir`` fixture). Now the directories are created with
|
||||||
|
private permissions.
|
|
@ -207,7 +207,7 @@ def _force_symlink(
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def make_numbered_dir(root: Path, prefix: str) -> Path:
|
def make_numbered_dir(root: Path, prefix: str, mode: int = 0o700) -> Path:
|
||||||
"""Create a directory with an increased number as suffix for the given prefix."""
|
"""Create a directory with an increased number as suffix for the given prefix."""
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
# try up to 10 times to create the folder
|
# try up to 10 times to create the folder
|
||||||
|
@ -215,7 +215,7 @@ def make_numbered_dir(root: Path, prefix: str) -> Path:
|
||||||
new_number = max_existing + 1
|
new_number = max_existing + 1
|
||||||
new_path = root.joinpath(f"{prefix}{new_number}")
|
new_path = root.joinpath(f"{prefix}{new_number}")
|
||||||
try:
|
try:
|
||||||
new_path.mkdir()
|
new_path.mkdir(mode=mode)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
@ -347,13 +347,13 @@ def cleanup_numbered_dir(
|
||||||
|
|
||||||
|
|
||||||
def make_numbered_dir_with_cleanup(
|
def make_numbered_dir_with_cleanup(
|
||||||
root: Path, prefix: str, keep: int, lock_timeout: float
|
root: Path, prefix: str, keep: int, lock_timeout: float, mode: int,
|
||||||
) -> Path:
|
) -> Path:
|
||||||
"""Create a numbered dir with a cleanup lock and remove old ones."""
|
"""Create a numbered dir with a cleanup lock and remove old ones."""
|
||||||
e = None
|
e = None
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
try:
|
try:
|
||||||
p = make_numbered_dir(root, prefix)
|
p = make_numbered_dir(root, prefix, mode)
|
||||||
lock_path = create_cleanup_lock(p)
|
lock_path = create_cleanup_lock(p)
|
||||||
register_cleanup_lock_removal(lock_path)
|
register_cleanup_lock_removal(lock_path)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|
|
@ -1426,7 +1426,7 @@ class Pytester:
|
||||||
:rtype: RunResult
|
:rtype: RunResult
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
p = make_numbered_dir(root=self.path, prefix="runpytest-")
|
p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700)
|
||||||
args = ("--basetemp=%s" % p,) + args
|
args = ("--basetemp=%s" % p,) + args
|
||||||
plugins = [x for x in self.plugins if isinstance(x, str)]
|
plugins = [x for x in self.plugins if isinstance(x, str)]
|
||||||
if plugins:
|
if plugins:
|
||||||
|
@ -1445,7 +1445,7 @@ class Pytester:
|
||||||
The pexpect child is returned.
|
The pexpect child is returned.
|
||||||
"""
|
"""
|
||||||
basetemp = self.path / "temp-pexpect"
|
basetemp = self.path / "temp-pexpect"
|
||||||
basetemp.mkdir()
|
basetemp.mkdir(mode=0o700)
|
||||||
invoke = " ".join(map(str, self._getpytestargs()))
|
invoke = " ".join(map(str, self._getpytestargs()))
|
||||||
cmd = f"{invoke} --basetemp={basetemp} {string}"
|
cmd = f"{invoke} --basetemp={basetemp} {string}"
|
||||||
return self.spawn(cmd, expect_timeout=expect_timeout)
|
return self.spawn(cmd, expect_timeout=expect_timeout)
|
||||||
|
|
|
@ -90,14 +90,14 @@ class TempPathFactory:
|
||||||
basename = self._ensure_relative_to_basetemp(basename)
|
basename = self._ensure_relative_to_basetemp(basename)
|
||||||
if not numbered:
|
if not numbered:
|
||||||
p = self.getbasetemp().joinpath(basename)
|
p = self.getbasetemp().joinpath(basename)
|
||||||
p.mkdir()
|
p.mkdir(mode=0o700)
|
||||||
else:
|
else:
|
||||||
p = make_numbered_dir(root=self.getbasetemp(), prefix=basename)
|
p = make_numbered_dir(root=self.getbasetemp(), prefix=basename, mode=0o700)
|
||||||
self._trace("mktemp", p)
|
self._trace("mktemp", p)
|
||||||
return p
|
return p
|
||||||
|
|
||||||
def getbasetemp(self) -> Path:
|
def getbasetemp(self) -> Path:
|
||||||
"""Return base temporary directory."""
|
"""Return the base temporary directory, creating it if needed."""
|
||||||
if self._basetemp is not None:
|
if self._basetemp is not None:
|
||||||
return self._basetemp
|
return self._basetemp
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ class TempPathFactory:
|
||||||
basetemp = self._given_basetemp
|
basetemp = self._given_basetemp
|
||||||
if basetemp.exists():
|
if basetemp.exists():
|
||||||
rm_rf(basetemp)
|
rm_rf(basetemp)
|
||||||
basetemp.mkdir()
|
basetemp.mkdir(mode=0o700)
|
||||||
basetemp = basetemp.resolve()
|
basetemp = basetemp.resolve()
|
||||||
else:
|
else:
|
||||||
from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT")
|
from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT")
|
||||||
|
@ -114,9 +114,13 @@ class TempPathFactory:
|
||||||
# use a sub-directory in the temproot to speed-up
|
# use a sub-directory in the temproot to speed-up
|
||||||
# make_numbered_dir() call
|
# make_numbered_dir() call
|
||||||
rootdir = temproot.joinpath(f"pytest-of-{user}")
|
rootdir = temproot.joinpath(f"pytest-of-{user}")
|
||||||
rootdir.mkdir(exist_ok=True)
|
rootdir.mkdir(mode=0o700, exist_ok=True)
|
||||||
basetemp = make_numbered_dir_with_cleanup(
|
basetemp = make_numbered_dir_with_cleanup(
|
||||||
prefix="pytest-", root=rootdir, keep=3, lock_timeout=LOCK_TIMEOUT
|
prefix="pytest-",
|
||||||
|
root=rootdir,
|
||||||
|
keep=3,
|
||||||
|
lock_timeout=LOCK_TIMEOUT,
|
||||||
|
mode=0o700,
|
||||||
)
|
)
|
||||||
assert basetemp is not None, basetemp
|
assert basetemp is not None, basetemp
|
||||||
self._basetemp = basetemp
|
self._basetemp = basetemp
|
||||||
|
|
|
@ -445,3 +445,19 @@ def test_basetemp_with_read_only_files(pytester: Pytester) -> None:
|
||||||
# running a second time and ensure we don't crash
|
# running a second time and ensure we don't crash
|
||||||
result = pytester.runpytest("--basetemp=tmp")
|
result = pytester.runpytest("--basetemp=tmp")
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not hasattr(os, "getuid"), reason="checks unix permissions")
|
||||||
|
def test_tmp_path_factory_create_directory_with_safe_permissions(
|
||||||
|
tmp_path: Path, monkeypatch,
|
||||||
|
) -> None:
|
||||||
|
"""Verify that pytest creates directories under /tmp with private permissions."""
|
||||||
|
# Use the test's tmp_path as the system temproot (/tmp).
|
||||||
|
monkeypatch.setenv("PYTEST_DEBUG_TEMPROOT", str(tmp_path))
|
||||||
|
tmp_factory = TempPathFactory(None, lambda *args: None, _ispytest=True)
|
||||||
|
basetemp = tmp_factory.getbasetemp()
|
||||||
|
|
||||||
|
# No world-readable permissions.
|
||||||
|
assert (basetemp.stat().st_mode & 0o077) == 0
|
||||||
|
# Parent too (pytest-of-foo).
|
||||||
|
assert (basetemp.parent.stat().st_mode & 0o077) == 0
|
||||||
|
|
Loading…
Reference in New Issue