|
16 | 16 | import tempfile |
17 | 17 | import shutil |
18 | 18 | import yaml |
| 19 | +import typer |
19 | 20 | from pathlib import Path |
20 | 21 | from unittest.mock import patch |
21 | 22 |
|
@@ -830,6 +831,39 @@ def test_codex_ai_skills_fresh_dir_does_not_create_codex_dir(self, tmp_path): |
830 | 831 | assert (target / ".specify").exists() |
831 | 832 | assert not (target / ".codex").exists() |
832 | 833 |
|
| 834 | + @pytest.mark.parametrize("is_current_dir", [False, True]) |
| 835 | + def test_download_and_extract_template_blocks_zip_path_traversal(self, tmp_path, monkeypatch, is_current_dir): |
| 836 | + """Extraction should reject ZIP members escaping the target directory.""" |
| 837 | + target = (tmp_path / "here-proj") if is_current_dir else (tmp_path / "new-proj") |
| 838 | + if is_current_dir: |
| 839 | + target.mkdir() |
| 840 | + monkeypatch.chdir(target) |
| 841 | + |
| 842 | + archive = tmp_path / "malicious-template.zip" |
| 843 | + with zipfile.ZipFile(archive, "w") as zf: |
| 844 | + zf.writestr("../evil.txt", "pwned") |
| 845 | + zf.writestr("template-root/.specify/templates/constitution-template.md", "constitution") |
| 846 | + |
| 847 | + fake_meta = { |
| 848 | + "filename": archive.name, |
| 849 | + "size": archive.stat().st_size, |
| 850 | + "release": "vtest", |
| 851 | + "asset_url": "https://example.invalid/template.zip", |
| 852 | + } |
| 853 | + |
| 854 | + with patch("specify_cli.download_template_from_github", return_value=(archive, fake_meta)): |
| 855 | + with pytest.raises(typer.Exit): |
| 856 | + specify_cli.download_and_extract_template( |
| 857 | + target, |
| 858 | + "codex", |
| 859 | + "sh", |
| 860 | + is_current_dir=is_current_dir, |
| 861 | + skip_legacy_codex_prompts=True, |
| 862 | + verbose=False, |
| 863 | + ) |
| 864 | + |
| 865 | + assert not (tmp_path / "evil.txt").exists() |
| 866 | + |
833 | 867 | def test_commands_preserved_when_skills_fail(self, tmp_path): |
834 | 868 | """If skills fail, commands should NOT be removed (safety net).""" |
835 | 869 | from typer.testing import CliRunner |
|
0 commit comments