Files
Mobile-Security-Framework-M…/mobsf/DynamicAnalyzer/views/android/tests_common.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

292 lines
9.4 KiB
Python

# -*- coding: utf_8 -*-
"""Available Actions."""
import logging
import os
import re
from django.conf import settings
from django.views.decorators.http import require_http_methods
from mobsf.DynamicAnalyzer.views.common.shared import (
invalid_params,
send_response,
)
from mobsf.DynamicAnalyzer.views.android.operations import (
get_package_name,
)
from mobsf.DynamicAnalyzer.views.android.environment import (
Environment,
)
from mobsf.DynamicAnalyzer.views.android.tests_xposed import (
download_xposed_log,
)
from mobsf.DynamicAnalyzer.views.android.tests_tls import (
run_tls_tests,
)
from mobsf.DynamicAnalyzer.tools.webproxy import (
get_http_tools_url,
stop_httptools,
)
from mobsf.MobSF.security import cmd_injection_check
from mobsf.MobSF.utils import (
is_md5,
python_list,
)
from mobsf.StaticAnalyzer.models import StaticAnalyzerAndroid
from mobsf.MobSF.views.authentication import (
login_required,
)
from mobsf.MobSF.views.authorization import (
Permissions,
permission_required,
)
logger = logging.getLogger(__name__)
# AJAX
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def start_activity(request, api=False):
"""Launch a specific activity."""
try:
env = Environment()
activity = request.POST['activity']
md5_hash = request.POST['hash']
valid_md5 = is_md5(md5_hash)
valid_act = re.match(r'^[\w]+(\.[\w]+)*$', activity)
if not valid_act or not valid_md5:
return invalid_params(api)
app_dir = os.path.join(settings.UPLD_DIR, md5_hash + '/')
screen_dir = os.path.join(app_dir, 'screenshots-apk/')
if not os.path.exists(screen_dir):
os.makedirs(screen_dir)
logger.info('Launching Activity - %s', activity)
outfile = ('{}act-{}.png'.format(screen_dir, activity))
static_android_db = StaticAnalyzerAndroid.objects.get(
MD5=md5_hash)
package = static_android_db.PACKAGE_NAME
env.launch_n_capture(package, activity, outfile)
data = {'status': 'ok'}
except Exception as exp:
logger.exception('Start Activity')
data = {'status': 'failed', 'message': str(exp)}
return send_response(data, api)
# AJAX
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def start_deeplink(request, api=False):
"""Launch a specific deeplink."""
try:
env = Environment()
url = request.POST['url']
md5_hash = request.POST['hash']
valid_md5 = is_md5(md5_hash)
if cmd_injection_check(url) or not valid_md5:
return invalid_params(api)
env.adb_command(
['am', 'start', '-a',
'android.intent.action.VIEW',
'-d', url], True)
data = {'status': 'ok'}
except Exception as exp:
logger.exception('Start Activity')
data = {'status': 'failed', 'message': str(exp)}
return send_response(data, api)
# AJAX
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def activity_tester(request, api=False):
"""Exported & non exported activity Tester."""
data = {}
try:
env = Environment()
test = request.POST['test']
md5_hash = request.POST['hash']
if not is_md5(md5_hash):
return invalid_params(api)
app_dir = os.path.join(settings.UPLD_DIR, md5_hash + '/')
screen_dir = os.path.join(app_dir, 'screenshots-apk/')
if not os.path.exists(screen_dir):
os.makedirs(screen_dir)
static_android_db = StaticAnalyzerAndroid.objects.get(
MD5=md5_hash)
package = static_android_db.PACKAGE_NAME
iden = ''
if test == 'exported':
iden = 'Exported '
logger.info('Exported activity tester')
activities = python_list(static_android_db.EXPORTED_ACTIVITIES)
else:
logger.info('Activity tester')
activities = python_list(static_android_db.ACTIVITIES)
logger.info('Fetching %sactivities for %s', iden, package)
if not activities:
msg = 'No {}Activities found'.format(iden)
logger.info(msg)
data = {'status': 'failed',
'message': msg}
return send_response(data, api)
act_no = 0
logger.info('Starting %sActivity Tester...', iden)
logger.info('%s %sActivities Identified',
str(len(activities)), iden)
for activity in activities:
act_no += 1
logger.info(
'Launching %sActivity - %s. %s',
iden,
str(act_no),
activity)
if test == 'exported':
file_iden = 'expact'
else:
file_iden = 'act'
outfile = ('{}{}-{}.png'.format(
screen_dir,
file_iden,
act_no))
env.launch_n_capture(package, activity, outfile)
data = {'status': 'ok'}
except Exception as exp:
logger.exception('%sActivity tester', iden)
data = {'status': 'failed', 'message': str(exp)}
return send_response(data, api)
# AJAX
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def download_data(request, api=False):
"""Download Application Data from Device."""
logger.info('Downloading app data')
data = {}
try:
env = Environment()
md5_hash = request.POST['hash']
if not is_md5(md5_hash):
return invalid_params(api)
package = get_package_name(md5_hash)
if not package:
data = {'status': 'failed',
'message': 'App details not found in database'}
return send_response(data, api)
apk_dir = os.path.join(settings.UPLD_DIR, md5_hash + '/')
stop_httptools(get_http_tools_url(request))
files_loc = '/data/local/'
logger.info('Archiving files created by app')
env.adb_command(['tar', '-cvf', files_loc + package + '.tar',
'/data/data/' + package + '/'], True)
logger.info('Downloading Archive')
env.adb_command(['pull', files_loc + package + '.tar',
apk_dir + package + '.tar'])
logger.info('Stopping ADB server')
env.adb_command(['kill-server'])
data = {'status': 'ok'}
except Exception as exp:
logger.exception('Downloading application data')
data = {'status': 'failed', 'message': str(exp)}
return send_response(data, api)
# AJAX
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def collect_logs(request, api=False):
"""Collecting Data and Cleanup."""
logger.info('Collecting Data and Cleaning Up')
data = {}
try:
env = Environment()
md5_hash = request.POST['hash']
if not is_md5(md5_hash):
return invalid_params(api)
package = get_package_name(md5_hash)
if not package:
data = {'status': 'failed',
'message': 'App details not found in database'}
return send_response(data, api)
apk_dir = os.path.join(settings.UPLD_DIR, md5_hash + '/')
lout = os.path.join(apk_dir, 'logcat.txt')
dout = os.path.join(apk_dir, 'dump.txt')
logger.info('Downloading logcat logs')
logcat = env.adb_command(['logcat',
'-d',
package + ':V',
'*:*'])
with open(lout, 'wb') as flip:
flip.write(logcat)
logger.info('Downloading dumpsys logs')
dumpsys = env.adb_command(['dumpsys'], True)
with open(dout, 'wb') as flip:
flip.write(dumpsys)
if env.get_android_version() < 5:
download_xposed_log(apk_dir)
env.adb_command(['am', 'force-stop', package], True)
logger.info('Stopping app')
# Unset Global Proxy
env.unset_global_proxy()
data = {'status': 'ok'}
except Exception as exp:
logger.exception('Data Collection & Clean Up failed')
data = {'status': 'failed', 'message': str(exp)}
return send_response(data, api)
# AJAX
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def tls_tests(request, api=False):
"""Perform TLS tests."""
logger.info('Running TLS/SSL Security tests')
data = {}
package = None
try:
test_duration = 25
test_package = 'tls_tests'
env = Environment()
md5_hash = request.POST['hash']
if not is_md5(md5_hash):
return invalid_params(api)
package = get_package_name(md5_hash)
if not package:
data = {'status': 'failed',
'message': 'App details not found in database'}
return send_response(data, api)
res = run_tls_tests(
request,
md5_hash,
env,
package,
test_package,
test_duration,
)
data = {'status': 'ok', 'tls_tests': res}
except Exception as exp:
logger.exception('Checking Application Security in Transit')
data = {'status': 'failed', 'message': str(exp)}
finally:
logger.info('Test Completed. Resuming HTTPS Proxy')
env.configure_proxy(package, request)
return send_response(data, api)