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

333 lines
9.9 KiB
Python

# -*- coding: utf_8 -*-
"""Dynamic Analyzer Operations."""
import json
import logging
import os
import random
import shlex
import subprocess
import threading
from pathlib import Path
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.MobSF.security import is_attack_pattern
from mobsf.DynamicAnalyzer.views.android.environment import (
Environment,
)
from mobsf.MobSF.security import cmd_injection_check
from mobsf.MobSF.utils import (
docker_translate_localhost,
get_adb,
get_device,
is_md5,
is_number,
)
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__)
# Helpers
def get_package_name(checksum):
"""Get Package Name from DB or Device."""
try:
static_android_db = StaticAnalyzerAndroid.objects.get(
MD5=checksum)
return static_android_db.PACKAGE_NAME
except Exception:
pkg_file = Path(settings.DWD_DIR) / 'packages.json'
if not pkg_file.exists():
return None
with pkg_file.open(encoding='utf-8') as src:
packages = json.load(src)
if packages.get(checksum):
return packages[checksum][0]
return None
# AJAX
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def mobsfy(request, api=False):
"""Configure Instance for Dynamic Analysis."""
logger.info('MobSFying Android instance')
data = {}
msg = 'Connection failed'
try:
identifier = request.POST['identifier']
if cmd_injection_check(identifier):
# Additional Check, not required
data = {
'status': 'failed',
'message': 'Command Injection Detected',
}
return send_response(data, api)
identifier = docker_translate_localhost(identifier)
create_env = Environment(identifier)
if not create_env.connect_n_mount():
data = {'status': 'failed', 'message': msg}
return send_response(data, api)
version = create_env.mobsfy_init()
if not version:
data = {'status': 'failed', 'message': msg}
return send_response(data, api)
else:
data = {'status': 'ok', 'android_version': version}
except Exception as exp:
logger.exception('MobSFying Android instance failed')
data = {'status': 'failed', 'message': str(exp)}
return send_response(data, api)
# AJAX
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def execute_adb(request, api=False):
"""Execute ADB Commands."""
data = {'status': 'ok', 'message': ''}
cmd = request.POST['cmd']
if cmd:
args = [get_adb(),
'-s',
get_device()]
try:
proc = subprocess.Popen(
args + cmd.split(' '), # lgtm [py/command-line-injection]
stdout=subprocess.PIPE, # Expected, cmd execute inside VM/AVD
stderr=subprocess.PIPE)
stdout, stderr = proc.communicate()
except Exception:
logger.exception('Executing ADB Commands')
if stdout or stderr:
out = stdout or stderr
out = out.decode('utf8', 'ignore')
else:
out = ''
data = {'status': 'ok', 'message': out}
return send_response(data, api)
# AJAX
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def get_component(request):
"""Get Android Component."""
data = {}
try:
env = Environment()
comp = request.POST['component']
bin_hash = request.POST['hash']
if is_attack_pattern(comp) or not is_md5(bin_hash):
return invalid_params()
comp = env.android_component(bin_hash, comp)
data = {'status': 'ok', 'message': comp}
except Exception as exp:
logger.exception('Getting Android Component')
data = {'status': 'failed', 'message': str(exp)}
return send_response(data)
# AJAX
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def run_apk(request):
"""Run Android APK."""
data = {}
try:
env = Environment()
md5_hash = request.POST['hash']
if not is_md5(md5_hash):
return invalid_params()
pkg = get_package_name(md5_hash)
if not pkg:
return invalid_params()
env.run_app(pkg)
data = {'status': 'ok'}
except Exception as exp:
logger.exception('Running the App')
data = {'status': 'failed', 'message': str(exp)}
return send_response(data)
# AJAX
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def take_screenshot(request, api=False):
"""Take Screenshot."""
logger.info('Taking screenshot')
data = {}
try:
env = Environment()
bin_hash = request.POST['hash']
if not is_md5(bin_hash):
return invalid_params(api)
data = {}
rand_int = random.randint(1, 1000000)
screen_dir = os.path.join(settings.UPLD_DIR,
bin_hash + '/screenshots-apk/')
if not os.path.exists(screen_dir):
os.makedirs(screen_dir)
outile = '{}screenshot-{}.png'.format(
screen_dir,
str(rand_int))
env.screen_shot(outile)
logger.info('Screenshot captured')
data = {'status': 'ok'}
except Exception as exp:
logger.exception('Taking screenshot')
data = {'status': 'failed', 'message': str(exp)}
return send_response(data, api)
# AJAX
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def screen_cast(request):
"""ScreenCast."""
data = {
'status': 'failed',
'message': 'Failed to stream screen'}
try:
env = Environment()
b64dat = env.screen_stream()
data = {
'status': 'ok',
'message': f'data:image/png;base64,{b64dat}'}
except Exception as exp:
logger.exception('Screen streaming')
data['message'] = str(exp)
return send_response(data)
# AJAX
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def touch(request):
"""Sending Touch/Swipe/Text Events."""
data = {
'status': 'failed',
'message': '',
}
try:
env = Environment()
x = request.POST['x']
y = request.POST['y']
event = request.POST['event']
max_x = request.POST.get('max_x', 0)
max_y = request.POST.get('max_y', 0)
if event == 'text':
args = ['text', shlex.quote(x)]
else:
if (not is_number(x)
or not is_number(y)
or not is_number(max_x)
or not is_number(max_y)):
return data
# Should not be greater than max screen size
swipe_x = str(min(int(float(x)) + 500, int(float(max_x))))
swipe_y = str(min(int(float(y)) + 500, int(float(max_y))))
if event == 'enter':
args = ['keyevent', '66']
elif event == 'backspace':
args = ['keyevent', '67']
elif event == 'left':
args = ['keyevent', '21']
elif event == 'right':
args = ['keyevent', '22']
elif event == 'swipe_up':
args = ['swipe', x, y, x, swipe_y]
elif event == 'swipe_down':
args = ['swipe', x, swipe_y, x, y]
elif event == 'swipe_left':
args = ['swipe', x, y, swipe_x, y]
elif event == 'swipe_right':
args = ['swipe', swipe_x, y, x, y]
else:
args = ['tap', x, y]
threading.Thread(target=env.adb_command,
args=(['input'] + args, True),
daemon=True).start()
data = {'status': 'ok'}
except Exception as exp:
logger.exception('Sending Touchscreen Events')
data['message'] = str(exp)
return send_response(data)
# AJAX
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def mobsf_ca(request, api=False):
"""Install and Remove MobSF Proxy RootCA."""
data = {}
try:
env = Environment()
action = request.POST['action']
if action == 'install':
env.install_mobsf_ca(action)
data = {'status': 'ok', 'message': 'installed'}
elif action == 'remove':
env.install_mobsf_ca(action)
data = {'status': 'ok', 'message': 'removed'}
else:
data = {'status': 'failed',
'message': 'Action not supported'}
except Exception as exp:
logger.exception('MobSF RootCA Handler')
data = {'status': 'failed', 'message': str(exp)}
return send_response(data, api)
# AJAX
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def global_proxy(request, api=False):
"""Set/unset global proxy."""
data = {}
try:
env = Environment()
version = env.get_android_version()
action = request.POST['action']
if action == 'set':
env.set_global_proxy(version)
data = {'status': 'ok', 'message': 'set'}
elif action == 'unset':
env.unset_global_proxy()
data = {'status': 'ok', 'message': 'unset'}
else:
data = {'status': 'failed',
'message': 'Action not supported'}
except Exception as exp:
logger.exception('MobSF Global Proxy')
data = {'status': 'failed', 'message': str(exp)}
return send_response(data, api)