Repository: spack/spack

Issue:
compression.py: do not extract absolute & relative .. paths
Don't extract by absolute paths, and do not allow `..` path components.

Note that the latter is possibly more strict than `tar -x`, the alternative would be to add a depth counter and make sure it's never negative. Another way to handle the former would be to strip the root `/` and still extract, but I don't wanna think about windows drive prefixes etc. 

Fixes a regression caused by Windows support.



Discussion:
As you mentioned, we could be stripping off leading `/` characters to then extract. IMO this approach is superior, as then we're not silently dropping files out of extracted archives and more closely respecting `tar -x` behavior. I wouldn't want consideration for Windows to be a reason to abandon this approach entirely. To handle on Windows, `os.path.splitdrive` can be used to remove the drive prefix, or some relatively straightforward manual parsing can be done. If this is something you consider unfeasible, then simply ignoring the Windows prefix case and leaving a todo note that Windows handling is still necessary is an approach I'd support as well.

 Edit: Feel free to call out my GH username explicitly in any TODOs relating to Windows.
`https://github.com/python/cpython/issues/94531` looks relevant to this (a recent PR to document the path-handling pitfalls of `tarfile`): that doesn't add anything actionable here but might be useful for folks looking through this later.
There's also a possible issue where a tar entry is a symlink to a dir outside the staging dir and has children of that symlinked dir (I haven't checked how `tar` behaves for this case...). I'm wondering whether it's better to temporarily revert the tarfile changes until these issues are figured out.

Note that of course during the build the build system can also execute arbitrary code and mess up your files, so it would make sense to always use overlayfs or a sandbox when building anything with Spack, but still it'd be nice if untarring and unzipping is guaranteed not to have side effects outside the stage dir.
> I'm wondering whether it's better to temporarily revert the tarfile changes until these issues are figured out.

I think this has merit, and in fact more than that, from reading through https://github.com/spack/spack/pull/31537#issuecomment-1182193854 and associated links more:

* The `tar` exe on Linux has considerations for the original problem (and also I think for https://github.com/spack/spack/pull/31537#issuecomment-1182514785) 
* The `tarfile` module doesn't (either by default or as options) and whether/how to add options to it is a matter of ongoing discussion
* I'd like to avoid reimplementing those checks (although I think you've handled the issue mentioned in https://github.com/spack/spack/pull/31537#issue-1301822517, I think https://github.com/spack/spack/pull/31537#issuecomment-1182514785 would be more complex to handle)

I'll try to handle that tomorrow (which would close this PR, although I assume that would be OK with you)

Resolving patch:
diff --git a/lib/spack/spack/util/compression.py b/lib/spack/spack/util/compression.py
--- a/lib/spack/spack/util/compression.py
+++ b/lib/spack/spack/util/compression.py
@@ -73,25 +73,10 @@ def _untar(archive_file):
     """
     _, ext = os.path.splitext(archive_file)
     outfile = os.path.basename(archive_file.strip(ext))
-    uncompress_required = 'Z' in ext
-    lzma_required = 'xz' in ext
-    lzma_needed_and_not_available = not lzma_support() and lzma_required
-    if tar_support() and not uncompress_required and\
-            not lzma_needed_and_not_available:
-        import tarfile
-
-        # Extract all members but wipe ownership info. This ensures we
-        # will not attempt to chown the files as superuser.
-        def filter(tarinfo):
-            tarinfo.uid = tarinfo.gid = 0
-            tarinfo.uname = tarinfo.gname = 'root'
-            return tarinfo
-        with tarfile.open(archive_file) as tar:
-            tar.extractall(members=map(filter, tar.getmembers()))
-    else:
-        tar = which('tar', required=True)
-        tar.add_default_arg('-oxf')
-        tar(archive_file)
+
+    tar = which('tar', required=True)
+    tar.add_default_arg('-oxf')
+    tar(archive_file)
     return outfile
 
 


Test patch: