Skip to content

Commit

Permalink
Manually resolves paths derived from root_dir to prevent rootfs escape
Browse files Browse the repository at this point in the history
This patch is a followup of #311.

It appeared that we were not resolving paths when reading from files.
This means that a symbolic link present under `root_dir` could be
blindly followed _outside_ of `root_dir`, possibly leading to host own
materials.
  • Loading branch information
Samuel FORESTIER committed Feb 18, 2022
1 parent 65eda6f commit 509cc4f
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 1 deletion.
28 changes: 28 additions & 0 deletions src/distro/distro.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
Callable,
Dict,
Iterable,
List,
Optional,
Sequence,
TextIO,
Expand Down Expand Up @@ -744,6 +745,9 @@ def __init__(
* :py:exc:`ValueError`: Initialization parameters combination is not
supported.
* :py:exc:`PermissionError`: At least a path is leading to outside
``root_dir``, if it has been specified.
* :py:exc:`OSError`: Some I/O issue with an os-release file or distro
release file.
Expand Down Expand Up @@ -791,6 +795,13 @@ def __init__(
include_oslevel if include_oslevel is not None else not is_root_dir_defined
)

self.__prevent_root_dir_escape(
root_dir,
[self.etc_dir, self.usr_lib_dir]
# only additionally check `self.os_release_file` if not user-supplied
+ ([self.os_release_file] if not os_release_file else []),
)

def __repr__(self) -> str:
"""Return repr of all info"""
return (
Expand All @@ -808,6 +819,22 @@ def __repr__(self) -> str:
"_oslevel_info={self._oslevel_info!r})".format(self=self)
)

@staticmethod
def __prevent_root_dir_escape(root_dir: Optional[str], paths: List[str]) -> None:
if root_dir is None:
return
# resolve paths derived from root_dir to prevent rootfs escape.
root_dir_resolved = os.path.realpath(root_dir)
if (
os.path.commonprefix(
[os.path.realpath(path) for path in paths] + [root_dir_resolved]
)
!= root_dir_resolved
):
raise PermissionError(
f"At least one path resolves to outside of {root_dir}."
)

def linux_distribution(
self, full_distribution_name: bool = True
) -> Tuple[str, str, str]:
Expand Down Expand Up @@ -1271,6 +1298,7 @@ def _distro_release_info(self) -> Dict[str, str]:
if match is None:
continue
filepath = os.path.join(self.etc_dir, basename)
self.__prevent_root_dir_escape(self.root_dir, [filepath])
distro_info = self._parse_distro_release_file(filepath)
# The name is always present if the pattern matches.
if "name" not in distro_info:
Expand Down
8 changes: 7 additions & 1 deletion tests/test_distro.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ def setup_method(self, test_method: FunctionType) -> None:
root_dir = os.path.join(DISTROS_DIR, dist)
self.distro = distro.LinuxDistribution(
os_release_file="",
distro_release_file="path-to-non-existing-file",
distro_release_file=os.path.join(root_dir, "path-to-non-existing-file"),
root_dir=root_dir,
)

Expand Down Expand Up @@ -701,6 +701,12 @@ def test_empty_release(self) -> None:
desired_outcome = {"id": "empty"}
self._test_outcome(desired_outcome)

def test_dontfollowsymlinks(self) -> None:
with pytest.raises(PermissionError):
distro.LinuxDistribution(
root_dir=os.path.join(TESTDISTROS, "distro", "dontfollowsymlinks")
)

def test_dontincludeuname(self) -> None:
self._setup_for_distro(os.path.join(TESTDISTROS, "distro", "dontincludeuname"))

Expand Down

0 comments on commit 509cc4f

Please sign in to comment.