Skip to content

Commit 5ab542e

Browse files
author
Samuel FORESTIER
committed
Manually resolves paths derived from root_dir to prevent rootfs escape
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.
1 parent 65eda6f commit 5ab542e

File tree

3 files changed

+35
-0
lines changed

3 files changed

+35
-0
lines changed

src/distro/distro.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
Callable,
4343
Dict,
4444
Iterable,
45+
List,
4546
Optional,
4647
Sequence,
4748
TextIO,
@@ -744,6 +745,9 @@ def __init__(
744745
* :py:exc:`ValueError`: Initialization parameters combination is not
745746
supported.
746747
748+
* :py:exc:`PermissionError`: At least a path is leading to outside
749+
``root_dir``, if it has been specified.
750+
747751
* :py:exc:`OSError`: Some I/O issue with an os-release file or distro
748752
release file.
749753
@@ -791,6 +795,13 @@ def __init__(
791795
include_oslevel if include_oslevel is not None else not is_root_dir_defined
792796
)
793797

798+
self.__prevent_root_dir_escape(
799+
root_dir,
800+
[self.etc_dir, self.usr_lib_dir]
801+
# only additionally check `self.os_release_file` if not user-supplied
802+
+ ([self.os_release_file] if not os_release_file else []),
803+
)
804+
794805
def __repr__(self) -> str:
795806
"""Return repr of all info"""
796807
return (
@@ -808,6 +819,22 @@ def __repr__(self) -> str:
808819
"_oslevel_info={self._oslevel_info!r})".format(self=self)
809820
)
810821

822+
@staticmethod
823+
def __prevent_root_dir_escape(root_dir: Optional[str], paths: List[str]) -> None:
824+
if root_dir is None:
825+
return
826+
# resolve paths derived from root_dir to prevent rootfs escape.
827+
root_dir_resolved = os.path.realpath(root_dir)
828+
if (
829+
os.path.commonprefix(
830+
[os.path.realpath(path) for path in paths] + [root_dir_resolved]
831+
)
832+
!= root_dir_resolved
833+
):
834+
raise PermissionError(
835+
f"At least one path resolves to outside of {root_dir}."
836+
)
837+
811838
def linux_distribution(
812839
self, full_distribution_name: bool = True
813840
) -> Tuple[str, str, str]:
@@ -1271,6 +1298,7 @@ def _distro_release_info(self) -> Dict[str, str]:
12711298
if match is None:
12721299
continue
12731300
filepath = os.path.join(self.etc_dir, basename)
1301+
self.__prevent_root_dir_escape(self.root_dir, [filepath])
12741302
distro_info = self._parse_distro_release_file(filepath)
12751303
# The name is always present if the pattern matches.
12761304
if "name" not in distro_info:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/etc/os-release

tests/test_distro.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,12 @@ def test_empty_release(self) -> None:
701701
desired_outcome = {"id": "empty"}
702702
self._test_outcome(desired_outcome)
703703

704+
def test_dontfollowsymlinks(self) -> None:
705+
with pytest.raises(PermissionError):
706+
distro.LinuxDistribution(
707+
root_dir=os.path.join(TESTDISTROS, "distro", "dontfollowsymlinks")
708+
)
709+
704710
def test_dontincludeuname(self) -> None:
705711
self._setup_for_distro(os.path.join(TESTDISTROS, "distro", "dontincludeuname"))
706712

0 commit comments

Comments
 (0)