diff --git a/src/distro/distro.py b/src/distro/distro.py index dfa8107..fdc14e4 100755 --- a/src/distro/distro.py +++ b/src/distro/distro.py @@ -42,6 +42,7 @@ Callable, Dict, Iterable, + List, Optional, Sequence, TextIO, @@ -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. @@ -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 ( @@ -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]: @@ -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: diff --git a/tests/resources/testdistros/distro/dontfollowsymlinks/etc/os-release b/tests/resources/testdistros/distro/dontfollowsymlinks/etc/os-release new file mode 120000 index 0000000..9751284 --- /dev/null +++ b/tests/resources/testdistros/distro/dontfollowsymlinks/etc/os-release @@ -0,0 +1 @@ +/etc/os-release \ No newline at end of file diff --git a/tests/test_distro.py b/tests/test_distro.py index d63e986..d85c28b 100644 --- a/tests/test_distro.py +++ b/tests/test_distro.py @@ -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, ) @@ -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"))