Files
Mobile-Security-Framework-M…/mobsf/DynamicAnalyzer/views/ios/device/report.py
T
Ajin Abraham 62563ca429 [HOTFIX][Security] Security Updates July 5 2026 (#2627)
Security Fixes

[GHSA-8j49-mmcx-4mp5] Arbitrary File Read via Path Traversal in ZIP/APK Icon Extraction
The android:icon attribute from an APK manifest was interpolated into file paths without validation, allowing a crafted APK to read arbitrary files from the server. Fixed by adding is_path_traversal() and is_safe_path() guards in find_icon_path_zip. Dependency bump included.

[GHSA-3p54-567p-2wpr] CSRF Checks Not Enforced After Django Middleware Migration
The migration from the deprecated MIDDLEWARE_CLASSES to MIDDLEWARE omitted CsrfViewMiddleware, SecurityMiddleware, and XFrameOptionsMiddleware, leaving CSRF, HSTS, and clickjacking protections silently disabled. All three have been restored to the active MIDDLEWARE tuple. The now-dead MIDDLEWARE_CLASSES block has been removed to prevent future confusion.

[GHSA-x768-8642-mmq9] Zip Bomb Denial of Service via Per-File Size Limit Bypass
The per-file size check in ZIP extraction logged a warning on oversized members but was missing a continue, allowing files exceeding ZIP_MAX_UNCOMPRESSED_FILE_SIZE to be extracted anyway as long as the aggregate limit was not reached. Fixed by adding the missing continue.

[GHSA-95px-34x5-p37h] SSRF Port Restriction Bypass in assetlinks_check
valid_host() was called with only the bare hostname, allowing android:port to be appended afterward without going through port validation, bypassing the HTTP/HTTPS-only restriction. Fixed with a two-layer approach: primary port allowlist check before URL assembly in get_browsable_activities, and a defence-in-depth re-check in _check_url.

Hardening

The hand-rolled safe_extract used os.path.abspath (symlink-blind) instead of os.path.realpath, creating a TOCTOU window where a symlink could redirect extraction outside the destination directory. Replaced with Python 3.12's tarfile.extractall(filter='data') (PEP 706), which rejects symlinks, hardlinks, absolute paths, and traversal members per-member before extraction. A robust realpath-based fallback is included for older Python versions.
Both download_app_data call sites in the iOS dynamic analyser now wrap app_container and tarfile with shlex.quote when building the SSH tar command, as defensive coding hygiene.
Refactoring

is_path_traversal, is_safe_path, clean_filename, cmd_injection_check, is_pipe_or_link, and is_attack_pattern were scattered across utils.py and shared.py. All have been moved to mobsf/MobSF/security.py as the single authoritative location for security primitives.
Developer Tooling

AGENTS.md / CLAUDE.md - AI agent guidelines
Added a coding-standards document for AI coding agents (Cursor, Claude, Codex) covering MobSF-specific secure-by-default patterns: path traversal guards, archive extraction safety, Django middleware active-tuple hygiene, split-validation anti-patterns (SSRF port bypass), guard completeness (continue/return/raise after every security check), and Django-specific security features (form validators, decorators, middleware). CLAUDE.md is a symlink to AGENTS.md.
2026-07-05 16:55:49 -07:00

315 lines
12 KiB
Python

# -*- coding: utf_8 -*-
"""Dynamic Analyzer Reporting for iOS devices."""
import logging
import shlex
import shutil
from pathlib import Path
import json
from django.conf import settings
from django.shortcuts import render
from django.template.defaulttags import register
from django.views.decorators.http import require_http_methods
from django.http import HttpResponse
import mobsf.MalwareAnalyzer.views.Trackers as Trackers
from mobsf.DynamicAnalyzer.views.ios.analysis import (
get_screenshots,
ios_api_analysis,
run_analysis,
)
from mobsf.DynamicAnalyzer.views.ios.device.dynamic_analyzer import (
FAILED,
OK,
validate_and_connect_device,
)
from mobsf.DynamicAnalyzer.tools.webproxy import (
get_http_tools_url,
stop_httptools,
)
from mobsf.MobSF.utils import (
IOS_DEVICE_ID_REGEX,
SSH_DEVICE_ID_REGEX,
base64_decode,
get_md5,
is_md5,
key,
pretty_json,
print_n_send_error_response,
replace,
strict_package_check,
)
from mobsf.MobSF.views.authentication import (
login_required,
)
from mobsf.DynamicAnalyzer.views.common.shared import (
send_response,
)
from mobsf.MobSF.views.authorization import (
Permissions,
permission_required,
)
logger = logging.getLogger(__name__)
register.filter('key', key)
register.filter('replace', replace)
register.filter('pretty_json', pretty_json)
register.filter('base64_decode', base64_decode)
def download_app_data_device(ios_device, checksum):
"""Download App data from device."""
app_dir = Path(settings.UPLD_DIR) / checksum
container_file = app_dir / 'mobsf_app_container_path.txt'
if container_file.exists():
app_container = container_file.read_text(
'utf-8').splitlines()[0].strip()
tarfile = f'/tmp/{checksum}-app-container.tar'
localtar = app_dir / f'{checksum}-app-container.tar'
ios_device.execute_command(
f'tar -C {shlex.quote(app_container)} -cvf {shlex.quote(tarfile)} .')
ios_device.download_file(tarfile, localtar)
if localtar.exists():
dst = Path(settings.DWD_DIR) / f'{checksum}-app_data.tar'
shutil.copyfile(localtar, dst)
# JSON API
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def download_data_device(request, bundle_id, api=False):
"""Download Application Data from Device."""
logger.info('Downloading application data')
data = {
'status': FAILED,
'message': 'Failed to Download application data'}
try:
device_id = request.POST['device_id']
ios_device, error = validate_and_connect_device(device_id)
if error:
data['message'] = error
return send_response(data, api)
if not strict_package_check(bundle_id):
data['message'] = 'Invalid iOS Bundle id'
return send_response(data, api)
checksum = get_md5(bundle_id.encode('utf-8'))
# App Container download
logger.info('Downloading app container data')
download_app_data_device(ios_device, checksum)
# Stop HTTPS Proxy
stop_httptools(get_http_tools_url(request))
# Move HTTP raw logs to download directory
flows = Path.home() / '.httptools' / 'flows'
webf = flows / f'{bundle_id}.flow.txt'
dwd = Path(settings.DWD_DIR)
dweb = dwd / f'{checksum}-web_traffic.txt'
if webf.exists():
shutil.copyfile(webf, dweb)
data = {
'status': OK,
'message': 'Downloaded application data',
}
except Exception as exp:
logger.exception('Downloading application data')
data['message'] = str(exp)
return send_response(data, api)
@login_required
def view_report_device(request, bundle_id, api=False):
"""Dynamic Analysis Report Generation."""
logger.info('iOS Dynamic Analysis Report Generation')
try:
if api:
device_id = request.POST.get('device_id')
else:
device_id = request.GET.get('device_id')
if device_id:
usb = IOS_DEVICE_ID_REGEX.match(device_id)
wifi = SSH_DEVICE_ID_REGEX.match(device_id)
if not (usb or wifi):
return print_n_send_error_response(
request,
'Invalid device id or SSH connection string format',
api)
if not strict_package_check(bundle_id):
# bundle_id is not validated in REST API.
# Also bundleid is not strictly validated
# in URL path.
return print_n_send_error_response(
request,
'Invalid iOS Bundle id',
api)
checksum = get_md5(bundle_id.encode('utf-8'))
app_dir = Path(settings.UPLD_DIR) / checksum
download_dir = settings.DWD_DIR
tools_dir = settings.TOOLS_DIR
frida_log = app_dir / 'mobsf_frida_out.txt'
data_dir = app_dir / 'DYNAMIC_DeviceData'
if not (frida_log.exists() or data_dir.exists()):
msg = ('Dynamic Analysis report is not available '
'for this app. Perform Dynamic Analysis '
'and generate the report.')
return print_n_send_error_response(request, msg, api)
api_analysis = ios_api_analysis(app_dir)
dump_analysis = run_analysis(app_dir, bundle_id, checksum)
trk = Trackers.Trackers(checksum, app_dir, tools_dir)
trackers = trk.get_trackers_domains_or_deps(
dump_analysis['domains'], None)
screenshots = get_screenshots(checksum, download_dir)
logger.info('Report generation completed')
context = {
'hash': checksum,
'device_id': device_id,
'version': settings.MOBSF_VER,
'title': 'iOS Dynamic Analysis Report',
'bundleid': bundle_id,
'trackers': trackers,
'screenshots': screenshots,
'frida_logs': frida_log.exists(),
}
context.update(api_analysis)
context.update(dump_analysis)
template = 'dynamic_analysis/ios/device/dynamic_report.html'
if api:
return context
return render(request, template, context)
except Exception as exp:
logger.exception('Dynamic Analysis Report Generation')
err = f'Error Generating Dynamic Analysis Report. {str(exp)}'
return print_n_send_error_response(request, err, api)
# JSON API + File Download
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def download_file_device(request, api=False):
"""Download file from device."""
err_msg = 'Failed to download file'
data = {
'status': FAILED,
'message': err_msg,
}
try:
remote_path = request.POST['file']
device_id = request.POST['device_id']
ios_device, error = validate_and_connect_device(device_id)
if error:
data['message'] = error
return send_response(data, api)
file_obj = ios_device.download_file_object(remote_path)
if file_obj:
response = HttpResponse(file_obj, content_type='application/octet-stream')
response['Content-Disposition'] = (
f'inline; filename={Path(remote_path).name}')
return response
else:
data['message'] = 'Failed to download file'
except Exception as exp:
logger.exception(err_msg)
data['message'] = str(exp)
return send_response(data, api)
# JSON API
@login_required
def ios_api_monitor(request, api=False):
try:
data = {
'status': FAILED,
'message': 'Failed to get API monitor data',
}
if api:
checksum = request.POST['checksum']
stream = True
else:
checksum = request.GET.get('checksum', '')
stream = request.GET.get('stream', '')
bundle_id = request.GET.get('bundle_id', '')
if bundle_id and not strict_package_check(bundle_id):
data['message'] = 'Invalid iOS Bundle id'
return send_response(data, api)
if not is_md5(checksum):
data['message'] = 'Invalid checksum format'
return send_response(data, api)
if stream:
app_dir = Path(settings.UPLD_DIR) / checksum
apimon_file = app_dir / 'mobsf_dump_file.txt'
data = {}
if not apimon_file.exists():
data['message'] = 'Data does not exist.'
return send_response(data, api)
# Read the file and parse each line as JSON
lines = apimon_file.read_text(encoding='utf-8').splitlines()
# remove all duplicate lines
lines = list(set(lines))
api_data = []
for line in lines:
if line.strip():
try:
# Parse each line as JSON
json_obj = json.loads(line)
# Determine the API type based on the keys in the JSON object
api_type = None
if 'cookies' in json_obj:
api_type = 'Cookies'
elif 'keychain' in json_obj:
api_type = 'Keychain'
elif 'json' in json_obj:
api_type = 'JSON Data'
elif 'network' in json_obj:
api_type = 'Network Request'
elif 'sql' in json_obj:
api_type = 'SQL Query'
elif 'filename' in json_obj:
api_type = 'File Access'
elif 'crypto' in json_obj:
api_type = 'Crypto'
elif 'files' in json_obj:
api_type = 'Files'
elif 'nslog' in json_obj:
api_type = 'Logs'
elif 'credentialstorage' in json_obj:
api_type = 'Credentials'
elif 'nsuserdefaults' in json_obj:
api_type = 'UserDefaults'
elif 'pasteboard' in json_obj:
api_type = 'Pasteboard'
elif 'textinput' in json_obj:
api_type = 'Text Inputs'
elif 'datadir' in json_obj:
api_type = 'Data Directory'
else:
# If no known key, use the first key as API type
api_type = list(json_obj.keys())[
0] if json_obj else 'Unknown'
# Create the data object for DataTable
api_data.append({
'api': api_type,
'data': json_obj,
})
except json.JSONDecodeError:
# Skip invalid JSON lines
continue
data['data'] = api_data
return send_response(data, api)
template = 'dynamic_analysis/ios/api_monitor.html'
return render(request,
template,
{'checksum': checksum,
'bundle_id': bundle_id,
'version': settings.MOBSF_VER,
'title': 'API Monitor'})
except Exception:
logger.exception('API monitor streaming')
err = 'Error in API monitor streaming'
return print_n_send_error_response(request, err, api)