|
1 | 1 | import json |
| 2 | +import plistlib |
2 | 3 | import tempfile |
3 | 4 |
|
4 | 5 | from pathlib import Path |
5 | 6 | from unittest.mock import patch |
6 | 7 |
|
| 8 | +import pytest |
| 9 | + |
7 | 10 | from launchpad.artifacts.apple.zipped_xcarchive import ZippedXCArchive |
| 11 | +from launchpad.artifacts.providers.zip_provider import UnsafePathError |
8 | 12 |
|
9 | 13 |
|
10 | 14 | class TestZippedXCArchive: |
@@ -145,3 +149,92 @@ def test_framework_bundle_asset_catalog_parsing(self) -> None: |
145 | 149 | assert not wrong_path.exists(), "Image should NOT exist at top-level" |
146 | 150 |
|
147 | 151 | assert "MyFramework.bundle" in str(element.full_path) |
| 152 | + |
| 153 | + def test_get_binary_path_rejects_path_traversal(self) -> None: |
| 154 | + with tempfile.TemporaryDirectory() as tmpdir: |
| 155 | + app_bundle_path = Path(tmpdir) / "Test.app" |
| 156 | + app_bundle_path.mkdir() |
| 157 | + |
| 158 | + with patch.object(ZippedXCArchive, "__init__", lambda self, path: None): |
| 159 | + archive = ZippedXCArchive(Path("dummy")) |
| 160 | + |
| 161 | + with ( |
| 162 | + patch.object(archive, "get_app_bundle_path", return_value=app_bundle_path), |
| 163 | + patch.object(archive, "get_plist", return_value={"CFBundleExecutable": "../../../etc/passwd"}), |
| 164 | + ): |
| 165 | + with pytest.raises(UnsafePathError): |
| 166 | + archive.get_binary_path() |
| 167 | + |
| 168 | + def test_get_main_binary_path_rejects_path_traversal(self) -> None: |
| 169 | + with tempfile.TemporaryDirectory() as tmpdir: |
| 170 | + app_bundle_path = Path(tmpdir) / "Test.app" |
| 171 | + app_bundle_path.mkdir() |
| 172 | + |
| 173 | + with patch.object(ZippedXCArchive, "__init__", lambda self, path: None): |
| 174 | + archive = ZippedXCArchive(Path("dummy")) |
| 175 | + |
| 176 | + with ( |
| 177 | + patch.object(archive, "get_app_bundle_path", return_value=app_bundle_path), |
| 178 | + patch.object(archive, "get_plist", return_value={"CFBundleExecutable": "../../../etc/passwd"}), |
| 179 | + ): |
| 180 | + with pytest.raises(UnsafePathError): |
| 181 | + archive._get_main_binary_path() |
| 182 | + |
| 183 | + def test_discover_extension_binaries_rejects_path_traversal(self) -> None: |
| 184 | + with tempfile.TemporaryDirectory() as tmpdir: |
| 185 | + app_bundle_path = Path(tmpdir) / "Test.app" |
| 186 | + extension_path = app_bundle_path / "PlugIns" / "Malicious.appex" |
| 187 | + extension_path.mkdir(parents=True) |
| 188 | + with open(extension_path / "Info.plist", "wb") as f: |
| 189 | + plistlib.dump({"CFBundleExecutable": "../../../etc/passwd"}, f) |
| 190 | + |
| 191 | + with patch.object(ZippedXCArchive, "__init__", lambda self, path: None): |
| 192 | + archive = ZippedXCArchive(Path("dummy")) |
| 193 | + with pytest.raises(UnsafePathError): |
| 194 | + archive._discover_extension_binaries(app_bundle_path) |
| 195 | + |
| 196 | + def test_discover_watch_binaries_rejects_path_traversal(self) -> None: |
| 197 | + with tempfile.TemporaryDirectory() as tmpdir: |
| 198 | + app_bundle_path = Path(tmpdir) / "Test.app" |
| 199 | + watch_path = app_bundle_path / "Watch" / "Malicious.app" |
| 200 | + watch_path.mkdir(parents=True) |
| 201 | + with open(watch_path / "Info.plist", "wb") as f: |
| 202 | + plistlib.dump({"CFBundleExecutable": "../../../etc/passwd"}, f) |
| 203 | + |
| 204 | + with patch.object(ZippedXCArchive, "__init__", lambda self, path: None): |
| 205 | + archive = ZippedXCArchive(Path("dummy")) |
| 206 | + with pytest.raises(UnsafePathError): |
| 207 | + archive._discover_watch_binaries(app_bundle_path) |
| 208 | + |
| 209 | + def test_asset_catalog_rejects_path_traversal_in_image_id(self) -> None: |
| 210 | + with tempfile.TemporaryDirectory() as tmpdir: |
| 211 | + tmpdir_path = Path(tmpdir) |
| 212 | + |
| 213 | + xcarchive_dir = tmpdir_path / "Test.xcarchive" |
| 214 | + parsed_assets_dir = xcarchive_dir / "ParsedAssets" / "Products" / "Applications" / "Test.app" |
| 215 | + parsed_assets_dir.mkdir(parents=True) |
| 216 | + |
| 217 | + assets_json = parsed_assets_dir / "Assets.json" |
| 218 | + assets_data = [ |
| 219 | + { |
| 220 | + "name": "icon.png", |
| 221 | + "imageId": "../../../etc/passwd", |
| 222 | + "size": 1024, |
| 223 | + "type": 0, |
| 224 | + "vector": False, |
| 225 | + "filename": "icon.png", |
| 226 | + } |
| 227 | + ] |
| 228 | + assets_json.write_text(json.dumps(assets_data)) |
| 229 | + |
| 230 | + with patch.object(ZippedXCArchive, "__init__", lambda self, path: None): |
| 231 | + archive = ZippedXCArchive(Path("dummy")) |
| 232 | + archive._extract_dir = tmpdir_path |
| 233 | + |
| 234 | + with patch.object( |
| 235 | + archive, |
| 236 | + "get_app_bundle_path", |
| 237 | + return_value=xcarchive_dir / "Products" / "Applications" / "Test.app", |
| 238 | + ): |
| 239 | + with pytest.raises(UnsafePathError): |
| 240 | + archive.get_asset_catalog_details(Path("Assets.car")) |
0 commit comments