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

296 lines
9.8 KiB
Python

# -*- coding: utf_8 -*-
"""Frida tests."""
import base64
import os
import re
import json
from pathlib import Path
from threading import Thread
import logging
from django.shortcuts import render
from django.conf import settings
from django.views.decorators.http import require_http_methods
from mobsf.DynamicAnalyzer.views.android.frida_core import Frida
from mobsf.DynamicAnalyzer.views.common.shared import (
invalid_params,
send_response,
)
from mobsf.MobSF.security import is_attack_pattern
from mobsf.DynamicAnalyzer.views.android.operations import (
get_package_name,
)
from mobsf.MobSF.utils import (
is_file_exists,
is_md5,
print_n_send_error_response,
strict_package_check,
)
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 get_runtime_dependencies(request, api=False):
"""Get App runtime dependencies."""
data = {
'status': 'failed',
'message': 'Failed to get runtime dependencies'}
try:
checksum = request.POST['hash']
if not is_md5(checksum):
return invalid_params(api)
package = get_package_name(checksum)
if not package:
return invalid_params(api)
get_dependencies(package, checksum)
return send_response(
{'status': 'ok'},
api)
except Exception:
pass
return send_response(data, api)
# AJAX
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def instrument(request, api=False):
"""Instrument app with frida."""
data = {
'status': 'failed',
'message': ''}
try:
action = request.POST.get('frida_action', 'spawn')
pid = request.POST.get('pid')
new_pkg = request.POST.get('new_package')
md5_hash = request.POST['hash']
default_hooks = request.POST['default_hooks']
auxiliary_hooks = request.POST['auxiliary_hooks']
code = request.POST['frida_code']
# Fill extras
extras = {}
class_name = request.POST.get('class_name')
if class_name:
extras['class_name'] = class_name.strip()
class_search = request.POST.get('class_search')
if class_search:
extras['class_search'] = class_search.strip()
cls_trace = request.POST.get('class_trace')
if cls_trace:
extras['class_trace'] = cls_trace.strip()
if (is_attack_pattern(default_hooks)
or is_attack_pattern(auxiliary_hooks)
or not is_md5(md5_hash)
or (new_pkg and not strict_package_check(new_pkg))):
return invalid_params(api)
package = get_package_name(md5_hash)
if not package and not new_pkg:
return invalid_params(api)
frida_obj = Frida(md5_hash,
package,
default_hooks.split(','),
auxiliary_hooks.split(','),
extras,
code)
if action == 'spawn':
logger.info('Starting Instrumentation')
frida_obj.spawn()
elif action == 'ps':
logger.info('Enumerating running applications')
data['message'] = frida_obj.ps()
elif action == 'get':
# Get injected Frida script.
data['message'] = frida_obj.get_script(nolog=True)
if action in ('spawn', 'session'):
if pid and pid.isdigit():
# Attach to a different pid/bundle id
args = (int(pid), new_pkg)
logger.info('Attaching to %s [PID: %s]', new_pkg, pid)
else:
# Injecting to existing session/spawn
if action == 'session':
logger.info('Injecting to existing frida session')
args = (None, None)
Thread(target=frida_obj.session, args=args, daemon=True).start()
data['status'] = 'ok'
except Exception as exp:
logger.exception('Instrumentation failed')
data = {'status': 'failed', 'message': str(exp)}
return send_response(data, api)
@login_required
def live_api(request, api=False):
try:
if api:
apphash = request.POST['hash']
stream = True
else:
apphash = request.GET.get('hash', '')
stream = request.GET.get('stream', '')
if not is_md5(apphash):
return invalid_params(api)
if stream:
apk_dir = os.path.join(settings.UPLD_DIR, apphash + '/')
apimon_file = os.path.join(apk_dir, 'mobsf_api_monitor.txt')
data = {}
if not is_file_exists(apimon_file):
data = {
'status': 'failed',
'message': 'Data does not exist.'}
return send_response(data, api)
with open(apimon_file, 'r',
encoding='utf8',
errors='ignore') as flip:
api_list = json.loads('[{}]'.format(
flip.read()[:-1]))
data = {'data': api_list}
return send_response(data, api)
logger.info('Starting API monitor streaming')
template = 'dynamic_analysis/android/live_api.html'
return render(request,
template,
{'hash': apphash,
'package': request.GET.get('package', ''),
'version': settings.MOBSF_VER,
'title': 'Live API Monitor'})
except Exception:
logger.exception('API monitor streaming')
err = 'Error in API monitor streaming'
return print_n_send_error_response(request, err, api)
def decode_base64(data, altchars=b'+/'):
"""Decode base64, padding being optional.
:param data: Base64 data as an ASCII byte string
:returns: The decoded byte string.
"""
data = re.sub(rb'[^a-zA-Z0-9%s]+' % altchars,
b'', data.encode())
missing_padding = len(data) % 4
if missing_padding:
data += b'=' * (4 - missing_padding)
return base64.b64decode(data, altchars)
def get_icon_map(name):
"""Get icon mapping."""
mapping = {
'Process': 'fas fa-chart-bar',
'Command': 'fas fa-terminal',
'Java Native Interface': 'fab fa-cuttlefish',
'WebView': 'far fa-window-maximize',
'File IO': 'fas fa-file-signature',
'Database': 'fas fa-database',
'IPC': 'fas fa-broadcast-tower',
'Binder': 'fas fa-cubes',
'Crypto': 'fas fa-lock',
'Crypto - Hash': 'fas fa-hashtag',
'Device Info': 'fas fa-info',
'Network': 'fas fa-wifi',
'Dex Class Loader': 'fas fa-asterisk',
'Base64': 'fas fa-puzzle-piece',
'System Manager': 'fas fa-cogs',
'SMS': 'fas fa-comment-alt',
'Device Data': 'fas fa-phone',
}
if name in mapping:
return mapping[name]
return 'far fa-dot-circle'
def apimon_analysis(app_dir):
"""API Analysis."""
api_details = {}
try:
strings = []
location = os.path.join(app_dir, 'mobsf_api_monitor.txt')
if not is_file_exists(location):
return api_details, strings
logger.info('Frida API Monitor Analysis')
with open(location, 'r',
encoding='utf8',
errors='ignore') as flip:
apis = json.loads('[{}]'.format(
flip.read()[:-1]))
for api in apis:
to_decode = None
if (api['class'] == 'android.util.Base64'
and (api['method'] == 'encodeToString')):
if api.get('returnValue'):
to_decode = api['returnValue'].replace('"', '')
elif (api['class'] == 'android.util.Base64'
and api['method'] == 'decode'):
to_decode = api['arguments'][0]
try:
if to_decode:
api['decoded'] = decode_base64(
to_decode).decode('utf-8', 'ignore')
strings.append((api['calledFrom'], api['decoded']))
except Exception:
pass
api['icon'] = get_icon_map(api['name'])
if api['name'] in api_details:
api_details[api['name']].append(api)
else:
api_details[api['name']] = [api]
except Exception:
logger.exception('API Monitor Analysis')
return api_details, strings
def get_dependencies(package, checksum):
"""Get 3rd party dependencies at runtime."""
frd = Frida(
checksum,
package,
['ssl_pinning_bypass', 'debugger_check_bypass', 'root_bypass'],
['get_dependencies'],
None,
None,
)
location = Path(frd.deps)
if location.exists():
location.write_text('')
frd.spawn()
Thread(target=frd.session, args=(None, None), daemon=True).start()
def dependency_analysis(package, app_dir):
deps = set()
msg = 'Collecting Runtime Dependency Analysis data'
try:
ignore = (
package,
'android.', 'androidx.', 'kotlin.', 'kotlinx.', 'java.', 'javax.',
'sun.', 'com.android.', 'j$', 'dalvik.system.', 'libcore.',
'com.google.', 'org.kxml2.', 'org.apache.', 'org.json.')
location = Path(app_dir) / 'mobsf_app_deps.txt'
if not location.exists():
return deps
logger.info(msg)
data = location.read_text('utf-8', 'ignore').splitlines()
for dep in data:
if not dep.startswith(ignore):
deps.add(dep.rsplit('.', 1)[0])
except Exception:
logger.exception(msg)
return deps