Compare commits

...

9 Commits
6.2.2 ... 6.2.3

Author SHA1 Message Date
pytest bot
3a2fd96305 Prepare release version 6.2.3 2021-04-03 21:41:18 +00:00
Ran Benita
138b19a930 Merge pull request #8517 from bluetech/backport-mktmp
[6.2.x] Fix minor temporary directory security issue
2021-04-04 00:34:15 +03:00
Ran Benita
822686e880 tmpdir: prevent using a non-private root temp directory
pytest uses a root temp directory named `/tmp/pytest-of-<username>`. The
name is predictable, and the directory might already exists from a
previous run, so that's allowed.

This makes it possible for my_user to pre-create
`/tmp/pytest-of-another_user`, thus giving my_user control of
another_user's tempdir.

Prevent this scenario by adding a couple of safety checks. I believe
they are sufficient.

Testing the first check requires changing the owner, which requires
root permissions, so can't be unit-tested easily, but I checked it
manually.
2021-04-04 00:04:50 +03:00
Ran Benita
9dc54f79b0 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.
2021-04-04 00:00:42 +03:00
Ran Benita
93dbae24e1 pathlib: inline ensure_reset_dir()
This is only used in TempPathFactory.getbasetemp(). We'll be wanting
further control/care there, so move it into there.
2021-04-03 23:39:37 +03:00
Ran Benita
02fdbe2e76 pathlib: remove useless temporary variable 2021-04-03 23:39:32 +03:00
Bruno Oliveira
12e7db85af Merge pull request #8285 from nicoddemus/backport-8280
[6.2.x] Doc: Move the module declaration to index.rst
2021-01-27 09:13:27 -03:00
Bruno Oliveira
56e4392444 Merge pull request #8280 from xuhdev/module
Doc: Move the module declaration to index.rst
2021-01-27 09:07:33 -03:00
Bruno Oliveira
8220eca963 Merge pull request #8275 from pytest-dev/release-6.2.2
Prepare release 6.2.2
2021-01-25 11:52:23 -03:00
11 changed files with 127 additions and 30 deletions

View File

@@ -6,6 +6,7 @@ Release announcements
:maxdepth: 2
release-6.2.3
release-6.2.2
release-6.2.1
release-6.2.0

View File

@@ -0,0 +1,19 @@
pytest-6.2.3
=======================================
pytest 6.2.3 has just been released to PyPI.
This is a bug-fix release, being a drop-in replacement. To upgrade::
pip install --upgrade pytest
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
Thanks to all of the contributors to this release:
* Bruno Oliveira
* Ran Benita
Happy testing,
The pytest Development Team

View File

@@ -28,6 +28,24 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start
pytest 6.2.3 (2021-04-03)
=========================
Bug Fixes
---------
- `#8414 <https://github.com/pytest-dev/pytest/issues/8414>`_: 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.
pytest used silenty use a pre-existing ``/tmp/pytest-of-<username>`` directory,
even if owned by another user. This means another user could pre-create such a
directory and gain control of another user's temporary directory. Now such a
condition results in an error.
pytest 6.2.2 (2021-01-25)
=========================

View File

@@ -508,11 +508,12 @@ Running it results in some skips if we don't have all the python interpreters in
.. code-block:: pytest
. $ pytest -rs -q multipython.py
ssssssssssss...ssssssssssss [100%]
sssssssssssssssssssssssssss [100%]
========================= short test summary info ==========================
SKIPPED [12] multipython.py:29: 'python3.5' not found
SKIPPED [12] multipython.py:29: 'python3.7' not found
3 passed, 24 skipped in 0.12s
SKIPPED [9] multipython.py:29: 'python3.5' not found
SKIPPED [9] multipython.py:29: 'python3.6' not found
SKIPPED [9] multipython.py:29: 'python3.7' not found
27 skipped in 0.12s
Indirect parametrization of optional implementations/imports
--------------------------------------------------------------------

View File

@@ -28,7 +28,7 @@ Install ``pytest``
.. code-block:: bash
$ pytest --version
pytest 6.2.2
pytest 6.2.3
.. _`simpletest`:

View File

@@ -11,6 +11,7 @@
pytest: helps you write better programs
=======================================
.. module:: pytest
The ``pytest`` framework makes it easy to write small tests, yet
scales to support complex functional testing for applications and libraries.

View File

@@ -3,8 +3,6 @@
API Reference
=============
.. module:: pytest
This page contains the full reference to pytest's API.
.. contents::

View File

@@ -64,13 +64,6 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
return path.joinpath(".lock")
def ensure_reset_dir(path: Path) -> None:
"""Ensure the given path is an empty directory."""
if path.exists():
rm_rf(path)
path.mkdir()
def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
"""Handle known read-only errors during rmtree.
@@ -214,7 +207,7 @@ def _force_symlink(
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."""
for i in range(10):
# try up to 10 times to create the folder
@@ -222,7 +215,7 @@ def make_numbered_dir(root: Path, prefix: str) -> Path:
new_number = max_existing + 1
new_path = root.joinpath(f"{prefix}{new_number}")
try:
new_path.mkdir()
new_path.mkdir(mode=mode)
except Exception:
pass
else:
@@ -354,13 +347,13 @@ def cleanup_numbered_dir(
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:
"""Create a numbered dir with a cleanup lock and remove old ones."""
e = None
for i in range(10):
try:
p = make_numbered_dir(root, prefix)
p = make_numbered_dir(root, prefix, mode)
lock_path = create_cleanup_lock(p)
register_cleanup_lock_removal(lock_path)
except Exception as exc:

View File

@@ -1426,7 +1426,7 @@ class Pytester:
:rtype: RunResult
"""
__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
plugins = [x for x in self.plugins if isinstance(x, str)]
if plugins:
@@ -1445,7 +1445,7 @@ class Pytester:
The pexpect child is returned.
"""
basetemp = self.path / "temp-pexpect"
basetemp.mkdir()
basetemp.mkdir(mode=0o700)
invoke = " ".join(map(str, self._getpytestargs()))
cmd = f"{invoke} --basetemp={basetemp} {string}"
return self.spawn(cmd, expect_timeout=expect_timeout)

View File

@@ -8,10 +8,10 @@ from typing import Optional
import attr
import py
from .pathlib import ensure_reset_dir
from .pathlib import LOCK_TIMEOUT
from .pathlib import make_numbered_dir
from .pathlib import make_numbered_dir_with_cleanup
from .pathlib import rm_rf
from _pytest.compat import final
from _pytest.config import Config
from _pytest.deprecated import check_ispytest
@@ -90,20 +90,22 @@ class TempPathFactory:
basename = self._ensure_relative_to_basetemp(basename)
if not numbered:
p = self.getbasetemp().joinpath(basename)
p.mkdir()
p.mkdir(mode=0o700)
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)
return p
def getbasetemp(self) -> Path:
"""Return base temporary directory."""
"""Return the base temporary directory, creating it if needed."""
if self._basetemp is not None:
return self._basetemp
if self._given_basetemp is not None:
basetemp = self._given_basetemp
ensure_reset_dir(basetemp)
if basetemp.exists():
rm_rf(basetemp)
basetemp.mkdir(mode=0o700)
basetemp = basetemp.resolve()
else:
from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT")
@@ -112,14 +114,37 @@ class TempPathFactory:
# use a sub-directory in the temproot to speed-up
# make_numbered_dir() call
rootdir = temproot.joinpath(f"pytest-of-{user}")
rootdir.mkdir(exist_ok=True)
rootdir.mkdir(mode=0o700, exist_ok=True)
# 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.
if hasattr(os, "getuid"):
rootdir_stat = rootdir.stat()
uid = os.getuid()
# getuid shouldn't fail, but cpython defines such a case.
# Let's hope for the best.
if uid != -1:
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)
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
self._basetemp = t = basetemp
self._trace("new basetemp", t)
return t
self._basetemp = basetemp
self._trace("new basetemp", basetemp)
return basetemp
@final

View File

@@ -445,3 +445,44 @@ def test_basetemp_with_read_only_files(pytester: Pytester) -> None:
# running a second time and ensure we don't crash
result = pytester.runpytest("--basetemp=tmp")
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
@pytest.mark.skipif(not hasattr(os, "getuid"), reason="checks unix permissions")
def test_tmp_path_factory_fixes_up_world_readable_permissions(
tmp_path: Path, monkeypatch,
) -> None:
"""Verify that if a /tmp/pytest-of-foo directory already exists with
world-readable permissions, it is fixed.
pytest used to mkdir with such permissions, that's why we fix it up.
"""
# 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()
# Before - simulate bad perms.
os.chmod(basetemp.parent, 0o777)
assert (basetemp.parent.stat().st_mode & 0o077) != 0
tmp_factory = TempPathFactory(None, lambda *args: None, _ispytest=True)
basetemp = tmp_factory.getbasetemp()
# After - fixed.
assert (basetemp.parent.stat().st_mode & 0o077) == 0