Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/launchpad/artifacts/android/aab.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from launchpad.utils.logging import get_logger

from ..artifact import AndroidArtifact
from ..providers.zip_provider import ZipProvider
from ..providers.zip_provider import UnsafePathError, ZipProvider, is_safe_path
from .apk import APK
from .manifest.manifest import AndroidManifest
from .manifest.proto_xml import ProtoXmlUtils
Expand Down Expand Up @@ -156,7 +156,11 @@ def get_app_icon(self) -> bytes | None:
logger.info("No icon path found in manifest")
return None

icon_path = self._extract_dir / "base" / icon_path_str
base_dir = self._extract_dir / "base"
if not is_safe_path(base_dir, icon_path_str):
raise UnsafePathError(f"Unsafe icon path in manifest: {icon_path_str}")

icon_path = base_dir / icon_path_str

if not icon_path.exists():
logger.info(f"Icon not found in AAB: {icon_path_str}")
Expand All @@ -175,6 +179,8 @@ def get_app_icon(self) -> bytes | None:

logger.info(f"Could not process XML drawable for icon: {icon_path_str}")
return None
except UnsafePathError:
raise
except Exception:
logger.exception(f"Error processing XML drawable for icon: {icon_path_str}")
return None
Expand Down
7 changes: 6 additions & 1 deletion src/launchpad/artifacts/android/apk.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from ...parsers.android.dex.types import ClassDefinition
from ...utils.logging import get_logger
from ..artifact import AndroidArtifact
from ..providers.zip_provider import ZipProvider
from ..providers.zip_provider import UnsafePathError, ZipProvider, is_safe_path
from .manifest.axml import AxmlUtils
from .manifest.manifest import AndroidManifest
from .resources.binary import BinaryResourceTable
Expand Down Expand Up @@ -128,6 +128,9 @@ def get_app_icon(self) -> bytes | None:
logger.info("No icon path found in manifest")
return None

if not is_safe_path(self._extract_dir, icon_path_str):
raise UnsafePathError(f"Unsafe icon path in manifest: {icon_path_str}")

icon_path = self._extract_dir / icon_path_str

if not icon_path.exists():
Expand All @@ -147,6 +150,8 @@ def get_app_icon(self) -> bytes | None:

logger.info(f"Could not process XML drawable for icon: {icon_path_str}")
return None
except UnsafePathError:
raise
except Exception:
logger.exception(f"Error processing XML drawable for icon: {icon_path_str}")
return None
Expand Down
5 changes: 4 additions & 1 deletion src/launchpad/artifacts/apple/zipped_xcarchive.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from launchpad.utils.logging import get_logger

from ..artifact import AppleArtifact
from ..providers.zip_provider import ZipProvider
from ..providers.zip_provider import UnsafePathError, ZipProvider, is_safe_path

logger = get_logger(__name__)

Expand Down Expand Up @@ -130,6 +130,9 @@ def get_app_icon(self) -> bytes | None:
app_bundle_path = self.get_app_bundle_path()

for icon_name in icon_info.primary_icon_files:
if not is_safe_path(app_bundle_path, icon_name):
raise UnsafePathError(f"Unsafe icon name in plist: {icon_name}")

# iOS lists base names without extensions or resolution modifiers (@2x, @3x, ~ipad)
# Search for files matching the base name with any suffix
# e.g., "AppIcon60x60" matches "AppIcon60x60@2x.png" or "AppIcon60x60.png"
Expand Down
6 changes: 6 additions & 0 deletions src/launchpad/parsers/android/icon/icon_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from PIL import Image, ImageDraw

from launchpad.artifacts.providers.zip_provider import UnsafePathError, is_safe_path
from launchpad.parsers.android.binary.types import XmlNode
from launchpad.utils.logging import get_logger

Expand Down Expand Up @@ -79,6 +80,8 @@ def render_from_path(self, xml_file_path: Path) -> bytes | None:
return self._render_adaptive_icon(root_node)

return self._render_vector_drawable(root_node)
except UnsafePathError:
raise
except Exception:
logger.exception("Error rendering icon from path")
return None
Expand Down Expand Up @@ -458,6 +461,9 @@ def _interpolate_gradient_color(
return None

def _find_file(self, filename: str) -> Path | None:
if not is_safe_path(self.extract_dir, filename):
raise UnsafePathError(f"Unsafe file path in drawable: {filename}")
Comment thread
sentry[bot] marked this conversation as resolved.

# Try exact match first
exact_path = self.extract_dir / filename
if exact_path.exists():
Expand Down
10 changes: 10 additions & 0 deletions tests/unit/artifacts/android/test_aab.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from pathlib import Path
from unittest.mock import patch

import pytest

from launchpad.artifacts.android.aab import AAB
from launchpad.artifacts.android.manifest.manifest import AndroidApplication, AndroidManifest
from launchpad.artifacts.providers.zip_provider import UnsafePathError


@pytest.fixture
Expand Down Expand Up @@ -32,3 +35,10 @@ def test_get_app_icon(self, test_aab: AAB) -> None:
assert len(icon) > 0
assert icon.startswith(b"\x89PNG")
assert icon.endswith(b"IEND\xae\x42\x60\x82")

def test_get_app_icon_rejects_path_traversal(self, test_aab: AAB) -> None:
malicious_app = AndroidApplication.model_construct(icon_path="../../../etc/passwd")
malicious_manifest = AndroidManifest.model_construct(application=malicious_app)
with patch.object(test_aab, "get_manifest", return_value=malicious_manifest):
with pytest.raises(UnsafePathError):
test_aab.get_app_icon()
10 changes: 10 additions & 0 deletions tests/unit/artifacts/android/test_apk.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from pathlib import Path
from unittest.mock import patch

import pytest

from launchpad.artifacts.android.apk import APK
from launchpad.artifacts.android.manifest.manifest import AndroidApplication, AndroidManifest
from launchpad.artifacts.providers.zip_provider import UnsafePathError


@pytest.fixture
Expand Down Expand Up @@ -43,3 +46,10 @@ def test_get_app_icon(self, test_apk: APK) -> None:
assert len(icon) > 0
assert icon.startswith(b"\x89PNG")
assert icon.endswith(b"IEND\xae\x42\x60\x82")

def test_get_app_icon_rejects_path_traversal(self, test_apk: APK) -> None:
malicious_app = AndroidApplication.model_construct(icon_path="../../../etc/passwd")
malicious_manifest = AndroidManifest.model_construct(application=malicious_app)
with patch.object(test_apk, "get_manifest", return_value=malicious_manifest):
with pytest.raises(UnsafePathError):
test_apk.get_app_icon()
Empty file.
21 changes: 21 additions & 0 deletions tests/unit/parsers/android/icon/test_icon_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import tempfile

from pathlib import Path

import pytest

from launchpad.artifacts.providers.zip_provider import UnsafePathError
from launchpad.parsers.android.icon.icon_parser import IconParser


class TestIconParserFindFile:
def test_find_file_rejects_path_traversal(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
extract_dir = Path(tmpdir)
parser = IconParser(extract_dir)

with pytest.raises(UnsafePathError):
parser._find_file("../../etc/passwd")

with pytest.raises(UnsafePathError):
parser._find_file("/etc/passwd")
Loading