mirror of
https://github.com/MobSF/Mobile-Security-Framework-MobSF.git
synced 2026-10-12 02:06:59 +00:00
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:
@@ -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` |
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user