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 12b1f007b875b94d5b34ceeaf4dceb908a2a2a4b
Author: Jürg Billeter <[email protected]>
AuthorDate: Fri Aug 28 16:06:59 2026 +0200

    tar.py: Use custom extraction filter
    
    The `tar` filter catches attempts to extract files outside the
    destination directory but does not catch hardlink targets outside the
    destination directory. The `data` filter catches both but doesn't
    preserve symlinks, which may break, e.g., rootfs tarball extraction.
    
    Define a custom extraction filter that uses the `data` filter except for
    symlinks where the `tar` filter is used.
    
    This filter replaces the incomplete checks of `_assert_safe()`, which
    was invoked before extraction, which meant that the checks may already
    have been outdated during actual extraction.
---
 src/buildstream/plugins/sources/tar.py | 40 ++++++++++------------------------
 1 file changed, 11 insertions(+), 29 deletions(-)

diff --git a/src/buildstream/plugins/sources/tar.py 
b/src/buildstream/plugins/sources/tar.py
index 91185a733..ae4f83ce4 100644
--- a/src/buildstream/plugins/sources/tar.py
+++ b/src/buildstream/plugins/sources/tar.py
@@ -149,42 +149,18 @@ class TarSource(DownloadableFileSource):
                     if base_dir and not base_dir.endswith(os.sep):
                         base_dir = base_dir + os.sep
 
-                filter_function = functools.partial(self._extract_filter, 
base_dir)
+                filter_function = functools.partial(self._preextract_filter, 
base_dir)
                 filtered_members = []
                 for member in tar.getmembers():
                     member = filter_function(member, directory)
                     if member is not None:
                         filtered_members.append(member)
-                tar.extractall(path=directory, members=filtered_members, 
filter="tar")
+                tar.extractall(path=directory, members=filtered_members, 
filter=self._extract_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(
+    def _preextract_filter(
         self, base_dir: Optional[str], member: tarfile.TarInfo, target_dir: str
     ) -> Optional[tarfile.TarInfo]:
         if base_dir:
@@ -216,14 +192,20 @@ class TarSource(DownloadableFileSource):
 
             member.path = member.path[L:]
 
-        self._assert_safe(member, target_dir)
-
         # Skip device nodes
         if member.isdev():
             return None
 
         return member
 
+    def _extract_filter(self, member: tarfile.TarInfo, target_dir: str) -> 
Optional[tarfile.TarInfo]:
+        if member.issym():
+            # `data_filter` is too restrictive as we need to preserve, e.g., 
absolute symbolic links.
+            # `tar_filter` still makes sure that symlinks aren't followed if 
they escape the target directory.
+            return tarfile.tar_filter(member, target_dir)
+        else:
+            return tarfile.data_filter(member, target_dir)
+
     # We want to iterate over all paths of a tarball, but getmembers()
     # is not enough because some tarballs simply do not contain the leading
     # directory paths for the archived files.

Reply via email to