mirror of
https://github.com/github/spec-kit.git
synced 2026-07-04 13:12:23 +08:00
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:
committed by
GitHub
parent
bd04937927
commit
05798a9e70
@@ -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}"
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user