This is an automated email from the ASF dual-hosted git repository. juergbi pushed a commit to branch jbilleter/tar in repository https://gitbox.apache.org/repos/asf/buildstream.git
commit 149b47f6b62af6b70530425f11bec7a6d35a4e28 Author: Jürg Billeter <[email protected]> AuthorDate: Fri Aug 28 16:06:59 2026 +0200 tar.py: Always use `tar_filter` `tar_filter` covers cases that `_assert_safe()` doesn't. As `tar_filter` is available in updates of all supported Python branches, always use it. Bail out early when running on an outdated Python version. The hardlink target check from `_assert_safe()` is still needed. Define a filter function that calls `tar_filter` and then performs a more robust version of the hardlink target check. Reported-by: Gjoko Krstic <[email protected]> --- src/buildstream/plugins/sources/tar.py | 52 ++++++++++++++-------------------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/src/buildstream/plugins/sources/tar.py b/src/buildstream/plugins/sources/tar.py index 040b5c48d..0c7493b5a 100644 --- a/src/buildstream/plugins/sources/tar.py +++ b/src/buildstream/plugins/sources/tar.py @@ -96,6 +96,22 @@ class ReadableTarInfo(tarfile.TarInfo): self.__permission = permission # pylint: disable=attribute-defined-outside-init +def _tar_safehardlink_filter(member: tarfile.TarInfo, target_dir: str) -> Optional[tarfile.TarInfo]: + # Refuse to extract files whose absolute path (after following symlinks) + # would end up outside the target directory (+ some other sanitization). + member = tarfile.tar_filter(member, target_dir) + + # Don't allow creating hardlinks of files outside the target directory. + # This would be covered by `tarfile.data_filter` but that filter is too + # restrictive. + if member and member.islnk(): + target_path = os.path.realpath(os.path.join(target_dir, member.linkname), strict=os.path.ALLOW_MISSING) + if os.path.commonpath([target_path, target_dir]) != target_dir: + raise tarfile.LinkOutsideDestinationError(member, target_path) + + return member + + class TarSource(DownloadableFileSource): # pylint: disable=attribute-defined-outside-init @@ -108,6 +124,11 @@ class TarSource(DownloadableFileSource): node.validate_keys(DownloadableFileSource.COMMON_CONFIG_KEYS + ["base-dir"]) def preflight(self): + if not hasattr(tarfile, "tar_filter"): + raise SourceError( + "Python version too old. Please upgrade to the latest security update of Python 3.10 or later." + ) + self.host_lzip = None if self.url.endswith(".lz"): self.host_lzip = utils.get_host_tool("lzip") @@ -150,38 +171,11 @@ class TarSource(DownloadableFileSource): member = filter_function(member, directory) if member is not None: filtered_members.append(member) - if sys.version_info >= (3, 12): - tar.extractall(path=directory, members=filtered_members, filter="tar") - else: - tar.extractall(path=directory, members=filtered_members) + tar.extractall(path=directory, members=filtered_members, filter=_tar_safehardlink_filter) except (tarfile.TarError, OSError) as e: raise SourceError("{}: Error staging source: {}".format(self, e)) from e - # Assert that a tarfile is safe to extract; specifically, make - # sure that we don't do anything outside of the target - # directory (this is possible, if, say, someone engineered a - # tarfile to contain paths that start with ..). - def _assert_safe(self, member: tarfile.TarInfo, target_dir: str): - final_path = os.path.abspath(os.path.join(target_dir, member.path)) - if not final_path.startswith(target_dir): - raise SourceError( - "{}: Tarfile attempts to extract outside the staging area: " - "{} -> {}".format(self, member.path, final_path) - ) - - if member.islnk(): - linked_path = os.path.abspath(os.path.join(target_dir, member.linkname)) - if not linked_path.startswith(target_dir): - raise SourceError( - "{}: Tarfile attempts to hardlink outside the staging area: " - "{} -> {}".format(self, member.path, final_path) - ) - - # Don't need to worry about symlinks because they're just - # files here and won't be able to do much harm once we are - # in a sandbox. - def _extract_filter( self, base_dir: Optional[str], member: tarfile.TarInfo, target_dir: str ) -> Optional[tarfile.TarInfo]: @@ -214,8 +208,6 @@ class TarSource(DownloadableFileSource): member.path = member.path[L:] - self._assert_safe(member, target_dir) - # Skip device nodes if member.isdev(): return None
