Bound androguard ZIP member decompression (#2675)

* Harden path traversal checks for Windows root-relative escapes.

Reject POSIX/Windows absolute paths in is_path_traversal and require post-join containment at icon, network-config, and archive extraction sinks.

Co-authored-by: Cursor <[email protected]>

* Restrict network security config names to a basename.

CodeQL flagged the manifest-derived join; reduce the resource name first so path construction cannot escape xml_dir.

Co-authored-by: Cursor <[email protected]>

* Use os.path.basename for CodeQL-recognized path sanitization.

Path.name did not clear py/path-injection taint; basename plus existing containment checks keep the network-config read inside xml_dir.

Co-authored-by: Cursor <[email protected]>

* Drop CodeQL-appeasement basename sanitization from network config reads.

is_safe_path after join already rejects traversal, absolute, and Windows root-relative names; CodeQL py/path-injection is a false positive.

Co-authored-by: Cursor <[email protected]>

* Bound androguard ZIP member decompression

Use the configured per-file ZIP limit for apkinspector reads and cap
actual raw-DEFLATE output. Keep decompression-limit failures out of the
tampered-entry fallback so malicious compressed data cannot be treated
as stored bytes.

Co-authored-by: Cursor <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Ajin Abraham
2026-09-20 16:32:50 -07:00
committed by GitHub
co-authored by Cursor
parent 388f26ad5e
commit a04a42e768
3 changed files with 50 additions and 5 deletions
+1
View File
@@ -10,6 +10,7 @@ Please report all security issues [here](https://github.com/MobSF/Mobile-Securit
| Vulnerability | Affected Versions |
| ------- | ------------------ |
| [Denial of service via APK decompression bomb in the bundled apkinspector ZIP reader](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-cvhc-xjjc-c4p3) | `<=4.5.3` |
| [Windows-only path traversal via root-relative paths bypasses is_path_traversal()](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-c8g7-42qj-frj3) | `<=4.5.3` |
| [SSRF port restriction bypass in assetlinks_check](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-95px-34x5-p37h) | `<=4.5.0` |
| [Zip Bomb Denial of Service via Per-File Size Limit Bypass in ZIP/APK](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-x768-8642-mmq9) | `<=4.5.0` |
+4
View File
@@ -3,3 +3,7 @@
class PathTraversalError(Exception):
"""Raised when a path traversal attempt is detected."""
class ZipBombError(Exception):
"""Raised when a ZIP member exceeds the decompression limit."""
@@ -3,7 +3,37 @@
import zlib
import os
from mobsf.MobSF.exceptions import PathTraversalError
from mobsf.MobSF.exceptions import PathTraversalError, ZipBombError
# Keep this fallback in sync with MobSF.settings.ZIP_MAX_UNCOMPRESSED_FILE_SIZE.
DEFAULT_MAX_UNCOMPRESSED_SIZE = 400 * 1024 * 1024
def _max_uncompressed_size():
"""Get MobSF's ZIP member limit without requiring Django at import time."""
try:
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
return settings.ZIP_MAX_UNCOMPRESSED_FILE_SIZE
except (ImportError, ImproperlyConfigured):
return DEFAULT_MAX_UNCOMPRESSED_SIZE
def _decompress_deflated(compressed_data, max_size):
"""Decompress a raw DEFLATE stream with a hard output ceiling."""
decompressor = zlib.decompressobj(-15)
extracted_data = decompressor.decompress(
compressed_data,
max_size + 1,
)
if len(extracted_data) > max_size or decompressor.unconsumed_tail:
raise ZipBombError(
'Decompressed ZIP member exceeds the configured size limit')
if not decompressor.eof:
raise zlib.error('Incomplete or truncated DEFLATE stream')
return extracted_data
def extract_file_based_on_header_info(apk_file, local_header_info, central_directory_info):
@@ -31,6 +61,11 @@ def extract_file_based_on_header_info(apk_file, local_header_info, central_direc
compressed_size = local_header_info["compressed_size"]
uncompressed_size = local_header_info["uncompressed_size"]
max_uncompressed_size = _max_uncompressed_size()
if uncompressed_size > max_uncompressed_size:
raise ZipBombError(
'ZIP member exceeds the configured size limit')
extra_field_length = local_header_info["extra_field_length"]
compression_method = local_header_info["compression_method"]
# Skip the offset + local header to reach the compressed data
@@ -43,8 +78,10 @@ def extract_file_based_on_header_info(apk_file, local_header_info, central_direc
indicator = 'STORED'
elif compression_method == 8:
compressed_data = apk_file.read(compressed_size)
# -15 for windows size due to raw stream with no header or trailer
extracted_data = zlib.decompress(compressed_data, -15)
extracted_data = _decompress_deflated(
compressed_data,
max_uncompressed_size,
)
indicator = 'DEFLATED'
elif compressed_size == uncompressed_size:
compressed_data = apk_file.read(uncompressed_size)
@@ -54,9 +91,12 @@ def extract_file_based_on_header_info(apk_file, local_header_info, central_direc
cur_loc = apk_file.tell()
try:
compressed_data = apk_file.read(compressed_size)
extracted_data = zlib.decompress(compressed_data, -15)
extracted_data = _decompress_deflated(
compressed_data,
max_uncompressed_size,
)
indicator = 'DEFLATED_TAMPERED'
except:
except zlib.error:
apk_file.seek(cur_loc)
compressed_data = apk_file.read(uncompressed_size)
extracted_data = compressed_data