Skip to content

Commit 2a9f60a

Browse files
committed
Threat: Fix zip slip in Android deploy wheel extraction
- Bare ZipFile.extractall() allows archive entries to write outside the target directory - Add safe_extractall() to android_helper.py; validate entries with Path.is_relative_to() - Replace bare extractall() in android_config.py with safe_extractall() - Add local safe_extractall() to PySide6 and shiboken6 recipe templates . This replace extractall() calls Task-number: PYSIDE-3319 Change-Id: I9ba59d91d74c839f5e0cdba4b06f2f2a962b7879 Reviewed-by: Friedemann Kleint <Friedemann.Kleint@qt.io> Reviewed-by: Ece Cinucen <ece.cinucen@qt.io>
1 parent d8bd45d commit 2a9f60a

5 files changed

Lines changed: 52 additions & 5 deletions

File tree

sources/pyside-tools/deploy_lib/android/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@
1616

1717
from .android_helper import (create_recipe, extract_and_copy_jar, get_wheel_android_arch,
1818
AndroidData, get_llvm_readobj, find_lib_dependencies,
19-
find_qtlibs_in_wheel)
19+
find_qtlibs_in_wheel, safe_extractall)
2020
from .android_config import AndroidConfig

sources/pyside-tools/deploy_lib/android/android_config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from . import (extract_and_copy_jar, get_wheel_android_arch, find_lib_dependencies,
1515
get_llvm_readobj, find_qtlibs_in_wheel, platform_map, create_recipe,
16-
ANDROID_DEPLOY_CACHE)
16+
ANDROID_DEPLOY_CACHE, safe_extractall)
1717
from .. import (Config, get_all_pyside_modules, MAJOR_VERSION)
1818
from .android_utilities import (ANDROID_NDK_VERSION, ANDROID_NDK_VERSION_NUMBER_SUFFIX,
1919
download_android_ndk)
@@ -307,7 +307,7 @@ def _find_dependent_qt_modules(self, modules: list[str]) -> list[str]:
307307
lib_path_suffix = Path(str(self.qt_libs_path)).relative_to(self.wheel_pyside)
308308

309309
with tempfile.TemporaryDirectory() as tmpdir:
310-
archive.extractall(tmpdir)
310+
safe_extractall(archive, Path(tmpdir))
311311
qt_libs_tmpdir = Path(tmpdir) / lib_path_suffix
312312
# find the lib folder where Qt libraries are stored
313313
for module_name in sorted(modules):

sources/pyside-tools/deploy_lib/android/android_helper.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,22 @@ def create_recipe(version: str, component: str, wheel_path: str, generated_files
5959
recipe.write(content)
6060

6161

62+
def safe_extractall(archive: ZipFile, target_path: Path) -> None:
63+
"""
64+
Extract all members of a zip archive into target_path, checking that each entry
65+
resolves inside target_path to prevent path traversal attacks.
66+
"""
67+
resolved_target = target_path.resolve()
68+
for member in archive.infolist():
69+
member_path = (target_path / member.filename).resolve()
70+
if not member_path.is_relative_to(resolved_target):
71+
raise RuntimeError(
72+
f"[DEPLOY] Refusing to extract '{member.filename}': "
73+
f"path resolves outside the extraction directory"
74+
)
75+
archive.extract(member, target_path)
76+
77+
6278
def extract_and_copy_jar(wheel_path: Path, generated_files_path: Path) -> str:
6379
'''
6480
extracts the PySide6 wheel and copies the 'jar' folder to 'generated_files_path'.

sources/pyside-tools/deploy_lib/android/recipes/PySide6/__init__.tmpl.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,21 @@
99
from pythonforandroid.logger import info
1010
from pythonforandroid.recipe import PythonRecipe
1111

12+
def safe_extractall(zip_ref: zipfile.ZipFile, target_path: Path) -> None:
13+
"""
14+
Extract all members of zip_ref into target_path, checking that each entry
15+
resolves inside target_path to prevent path traversal attacks.
16+
"""
17+
resolved_target = target_path.resolve()
18+
for member in zip_ref.infolist():
19+
member_path = (target_path / member.filename).resolve()
20+
if not member_path.is_relative_to(resolved_target):
21+
raise RuntimeError(
22+
f"Refusing to extract '{member.filename}': "
23+
f"path resolves outside the extraction directory"
24+
)
25+
zip_ref.extract(member, target_path)
26+
1227

1328
class PySideRecipe(PythonRecipe):
1429
version = '{{ version }}'
@@ -27,7 +42,7 @@ def build_arch(self, arch):
2742
info(f"Installing {self.name} into site-packages")
2843
with zipfile.ZipFile(self.wheel_path, "r") as zip_ref:
2944
info("Unzip wheels and copy into {}".format(self.ctx.get_python_install_dir(arch.arch)))
30-
zip_ref.extractall(self.ctx.get_python_install_dir(arch.arch))
45+
safe_extractall(zip_ref, Path(self.ctx.get_python_install_dir(arch.arch)))
3146

3247
lib_dir = Path(f"{self.ctx.get_python_install_dir(arch.arch)}/PySide6/Qt/lib")
3348

sources/pyside-tools/deploy_lib/android/recipes/shiboken6/__init__.tmpl.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,22 @@
1010
from pythonforandroid.recipe import PythonRecipe
1111

1212

13+
def safe_extractall(zip_ref: zipfile.ZipFile, target_path: Path) -> None:
14+
"""
15+
Extract all members of zip_ref into target_path, checking that each entry
16+
resolves inside target_path to prevent path traversal attacks.
17+
"""
18+
resolved_target = target_path.resolve()
19+
for member in zip_ref.infolist():
20+
member_path = (target_path / member.filename).resolve()
21+
if not member_path.is_relative_to(resolved_target):
22+
raise RuntimeError(
23+
f"Refusing to extract '{member.filename}': "
24+
f"path resolves outside the extraction directory"
25+
)
26+
zip_ref.extract(member, target_path)
27+
28+
1329
class ShibokenRecipe(PythonRecipe):
1430
version = '{{ version }}'
1531
wheel_path = '{{ wheel_path }}'
@@ -22,7 +38,7 @@ def build_arch(self, arch):
2238
info('Installing {} into site-packages'.format(self.name))
2339
with zipfile.ZipFile(self.wheel_path, 'r') as zip_ref:
2440
info('Unzip wheels and copy into {}'.format(self.ctx.get_python_install_dir(arch.arch)))
25-
zip_ref.extractall(self.ctx.get_python_install_dir(arch.arch))
41+
safe_extractall(zip_ref, Path(self.ctx.get_python_install_dir(arch.arch)))
2642

2743
lib_dir = Path(f"{self.ctx.get_python_install_dir(arch.arch)}/shiboken6")
2844
shutil.copyfile(lib_dir / "libshiboken6.abi3.so",

0 commit comments

Comments
 (0)