Skip PAX/GNU metadata members in safe_extract_tarball; use standard mock imports in workflow tests

Agent-Logs-Url: https://github.com/github/spec-kit/sessions/c1fcc1ff-8766-4d97-90a5-368447980acf

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-05-07 17:57:01 +00:00
committed by GitHub
parent bd04937927
commit 05798a9e70
2 changed files with 30 additions and 28 deletions

View File

@@ -170,6 +170,15 @@ def safe_extract_tarball(
error_class: If any member is unsafe or the archive cannot be read.
"""
dest_resolved = dest_dir.resolve()
# Tar metadata member types to skip during validation — they carry no
# extractable payload and are generated automatically by many common
# archiving tools (e.g. PAX headers, GNU longname/longlink entries).
_TAR_METADATA_TYPES = (
tarfile.XHDTYPE, # PAX extended header
tarfile.XGLTYPE, # PAX global extended header
tarfile.SOLARIS_XHDTYPE, # Solaris PAX extended header
*tarfile.GNU_TYPES, # GNU longname / longlink / sparse
)
try:
with tarfile.open(archive_path, "r:gz") as tf:
@@ -195,13 +204,17 @@ def safe_extract_tarball(
f"Unsafe path in tar archive: {member.name} (potential path traversal)"
)
# Skip tar metadata members — they carry no extractable payload.
if member.type in _TAR_METADATA_TYPES:
continue
# Reject symlinks and hard links.
if member.issym() or member.islnk():
raise error_class(
f"Symlinks are not allowed in archive: {member.name}"
)
# Only allow regular files and directories.
# Reject devices, FIFOs and other special file types.
if not (member.isreg() or member.isdir()):
raise error_class(
f"Non-regular file in archive: {member.name}"

View File

@@ -1882,15 +1882,14 @@ class TestWorkflowAddArchive:
def test_workflow_add_local_zip_flat(self, project_dir):
"""workflow add installs from a local ZIP with workflow.yml at root."""
import zipfile
from unittest.mock import patch
runner, app = self._runner_and_app()
archive = project_dir / "workflow.zip"
with zipfile.ZipFile(archive, "w") as zf:
zf.writestr("workflow.yml", MINIMAL_WORKFLOW_YAML)
with __import__("unittest.mock", fromlist=["patch"]).patch.object(
__import__("pathlib", fromlist=["Path"]).Path, "cwd", return_value=project_dir
):
with patch.object(Path, "cwd", return_value=project_dir):
result = runner.invoke(app, ["workflow", "add", str(archive)], catch_exceptions=False)
assert result.exit_code == 0, result.output
@@ -1901,15 +1900,14 @@ class TestWorkflowAddArchive:
def test_workflow_add_local_zip_nested(self, project_dir):
"""workflow add installs from a local ZIP with workflow.yml in a subdirectory."""
import zipfile
from unittest.mock import patch
runner, app = self._runner_and_app()
archive = project_dir / "workflow.zip"
with zipfile.ZipFile(archive, "w") as zf:
zf.writestr("repo-1.0/workflow.yml", MINIMAL_WORKFLOW_YAML)
with __import__("unittest.mock", fromlist=["patch"]).patch.object(
__import__("pathlib", fromlist=["Path"]).Path, "cwd", return_value=project_dir
):
with patch.object(Path, "cwd", return_value=project_dir):
result = runner.invoke(app, ["workflow", "add", str(archive)], catch_exceptions=False)
assert result.exit_code == 0, result.output
@@ -1918,15 +1916,14 @@ class TestWorkflowAddArchive:
def test_workflow_add_local_zip_missing_workflow_yml(self, project_dir):
"""workflow add exits with an error when the ZIP has no workflow.yml."""
import zipfile
from unittest.mock import patch
runner, app = self._runner_and_app()
archive = project_dir / "empty.zip"
with zipfile.ZipFile(archive, "w") as zf:
zf.writestr("README.md", "nothing here")
with __import__("unittest.mock", fromlist=["patch"]).patch.object(
__import__("pathlib", fromlist=["Path"]).Path, "cwd", return_value=project_dir
):
with patch.object(Path, "cwd", return_value=project_dir):
result = runner.invoke(app, ["workflow", "add", str(archive)], catch_exceptions=True)
assert result.exit_code != 0
@@ -1937,6 +1934,7 @@ class TestWorkflowAddArchive:
def test_workflow_add_local_tar_gz_flat(self, project_dir):
"""workflow add installs from a local .tar.gz with workflow.yml at root."""
import tarfile, io
from unittest.mock import patch
runner, app = self._runner_and_app()
archive = project_dir / "workflow.tar.gz"
@@ -1946,9 +1944,7 @@ class TestWorkflowAddArchive:
info.size = len(data)
tf.addfile(info, io.BytesIO(data))
with __import__("unittest.mock", fromlist=["patch"]).patch.object(
__import__("pathlib", fromlist=["Path"]).Path, "cwd", return_value=project_dir
):
with patch.object(Path, "cwd", return_value=project_dir):
result = runner.invoke(app, ["workflow", "add", str(archive)], catch_exceptions=False)
assert result.exit_code == 0, result.output
@@ -1959,6 +1955,7 @@ class TestWorkflowAddArchive:
def test_workflow_add_local_tar_gz_nested(self, project_dir):
"""workflow add installs from a local .tar.gz with workflow.yml in a subdirectory."""
import tarfile, io
from unittest.mock import patch
runner, app = self._runner_and_app()
archive = project_dir / "workflow.tar.gz"
@@ -1968,9 +1965,7 @@ class TestWorkflowAddArchive:
info.size = len(data)
tf.addfile(info, io.BytesIO(data))
with __import__("unittest.mock", fromlist=["patch"]).patch.object(
__import__("pathlib", fromlist=["Path"]).Path, "cwd", return_value=project_dir
):
with patch.object(Path, "cwd", return_value=project_dir):
result = runner.invoke(app, ["workflow", "add", str(archive)], catch_exceptions=False)
assert result.exit_code == 0, result.output
@@ -1979,6 +1974,7 @@ class TestWorkflowAddArchive:
def test_workflow_add_local_tgz_flat(self, project_dir):
"""workflow add recognises the .tgz extension as a gzipped tarball."""
import tarfile, io
from unittest.mock import patch
runner, app = self._runner_and_app()
archive = project_dir / "workflow.tgz"
@@ -1988,9 +1984,7 @@ class TestWorkflowAddArchive:
info.size = len(data)
tf.addfile(info, io.BytesIO(data))
with __import__("unittest.mock", fromlist=["patch"]).patch.object(
__import__("pathlib", fromlist=["Path"]).Path, "cwd", return_value=project_dir
):
with patch.object(Path, "cwd", return_value=project_dir):
result = runner.invoke(app, ["workflow", "add", str(archive)], catch_exceptions=False)
assert result.exit_code == 0, result.output
@@ -1999,6 +1993,7 @@ class TestWorkflowAddArchive:
def test_workflow_add_local_tar_gz_missing_workflow_yml(self, project_dir):
"""workflow add exits with an error when the .tar.gz has no workflow.yml."""
import tarfile, io
from unittest.mock import patch
runner, app = self._runner_and_app()
archive = project_dir / "empty.tar.gz"
@@ -2008,9 +2003,7 @@ class TestWorkflowAddArchive:
info.size = len(data)
tf.addfile(info, io.BytesIO(data))
with __import__("unittest.mock", fromlist=["patch"]).patch.object(
__import__("pathlib", fromlist=["Path"]).Path, "cwd", return_value=project_dir
):
with patch.object(Path, "cwd", return_value=project_dir):
result = runner.invoke(app, ["workflow", "add", str(archive)], catch_exceptions=True)
assert result.exit_code != 0
@@ -2041,9 +2034,7 @@ class TestWorkflowAddArchive:
mock_resp.__exit__ = MagicMock(return_value=False)
with patch("urllib.request.urlopen", return_value=mock_resp), \
patch.object(
__import__("pathlib", fromlist=["Path"]).Path, "cwd", return_value=project_dir
):
patch.object(Path, "cwd", return_value=project_dir):
result = runner.invoke(
app, ["workflow", "add", "https://example.com/workflow.tar.gz"],
catch_exceptions=False,
@@ -2071,9 +2062,7 @@ class TestWorkflowAddArchive:
mock_resp.__exit__ = MagicMock(return_value=False)
with patch("urllib.request.urlopen", return_value=mock_resp), \
patch.object(
__import__("pathlib", fromlist=["Path"]).Path, "cwd", return_value=project_dir
):
patch.object(Path, "cwd", return_value=project_dir):
result = runner.invoke(
app, ["workflow", "add", "https://example.com/workflow.zip"],
catch_exceptions=False,