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

185 lines
6.2 KiB
Python

# -*- coding: utf_8 -*-
"""Common helpers for Android and iOS Dynamic Analysis."""
import logging
import os
import re
import errno
import json
import tarfile
import shutil
from pathlib import Path
from django.http import HttpResponse
from mobsf.MalwareAnalyzer.views.MalwareDomainCheck import (
MalwareDomainCheck,
)
from mobsf.MobSF.exceptions import PathTraversalError
from mobsf.MobSF.security import clean_filename, is_pipe_or_link
from mobsf.MobSF.utils import (
EMAIL_REGEX,
URL_REGEX,
)
logger = logging.getLogger(__name__)
def extract_urls_domains_emails(checksum, data):
"""Extract URLs, Domains and Emails."""
# URL Extraction
urls = re.findall(URL_REGEX, data)
if urls:
urls = list(set(urls))
else:
urls = []
# Domain Extraction and Malware Check
logger.info('Performing Malware check on extracted domains')
# For domain extraction, use lowercased URLs
domains = MalwareDomainCheck().scan(
checksum,
urls)
# Email Extraction Regex
emails = set()
for email in EMAIL_REGEX.findall(data.lower()):
if email.startswith('//'):
continue
if email.endswith('.png'):
continue
emails.add(email)
return urls, domains, emails
def safe_paths(tar_meta):
"""Safe filenames in windows."""
for fh in tar_meta:
fh.name = clean_filename(fh.name)
yield fh
def onexc(func, path, exc_info):
_, exc_value, _ = exc_info
if exc_value.errno == errno.EACCES: # Permission error
try:
os.chmod(path, 0o755)
func(path)
except Exception:
pass
elif exc_value.errno == errno.ENOTEMPTY: # Directory not empty
try:
func(path)
except Exception:
pass
else:
raise
def untar_files(tar_loc, untar_dir):
"""Untar files."""
logger.info('Extracting Tar files')
try:
# Extract Device Data
if not tar_loc.exists():
return False
if untar_dir.exists():
# fix for permission errors
shutil.rmtree(untar_dir, onexc=onexc)
else:
os.makedirs(untar_dir)
with tarfile.open(tar_loc.as_posix(), errorlevel=1) as tar:
if hasattr(tarfile, 'data_filter'):
# Python 3.12+ (PEP 706) / backported to 3.11.4, 3.10.12, 3.9.17.
# Rejects symlinks outside destination, hardlinks, device files,
# and absolute/traversal paths per-member before extraction.
tar.extractall(
untar_dir,
members=safe_paths(tar),
filter='data')
else:
# Fallback for Python < 3.9.17 without the PEP 706 backport.
# Manually reject symlinks and hardlinks, then use realpath
# (not abspath) to verify the resolved path stays inside the
# destination before extracting each member.
safe_root = os.path.realpath(untar_dir)
safe_members = []
for member in safe_paths(tar):
if member.issym() or member.islnk():
logger.warning(
'Skipping link member in tar: %s', member.name)
continue
member_path = os.path.realpath(
os.path.join(safe_root, member.name))
if not (member_path.startswith(safe_root + os.sep)
or member_path == safe_root):
raise PathTraversalError(
'Attempted Path Traversal in Tar File')
safe_members.append(member)
tar.extractall(untar_dir, members=iter(safe_members))
except (FileExistsError, tarfile.ReadError):
logger.warning('Failed to extract tar file')
except Exception:
logger.exception('Tar extraction failed')
return True
def get_app_files(app_dir, tarname):
"""Get files from device."""
logger.info('Getting app files')
all_files = {'xml': [], 'sqlite': [], 'others': [], 'plist': []}
appdir = Path(app_dir)
tar_loc = appdir / f'{tarname}.tar'
untar_dir = appdir / 'DYNAMIC_DeviceData'
success = untar_files(tar_loc, untar_dir)
if not success:
return all_files
# Do Static Analysis on Data from Device
try:
untar_dir = untar_dir.as_posix()
for dir_name, _, files in os.walk(untar_dir):
for jfile in files:
file_path = os.path.join(untar_dir, dir_name, jfile)
fileparam = file_path.replace(f'{untar_dir}/', '')
if is_pipe_or_link(file_path):
continue
if jfile == 'lib':
pass
else:
if jfile.endswith('.xml'):
all_files['xml'].append(
{'type': 'xml', 'file': fileparam})
elif jfile.endswith('.plist'):
all_files['plist'].append(
{'type': 'plist', 'file': fileparam})
else:
with open(file_path,
'r',
encoding='ISO-8859-1') as flip:
file_cnt_sig = flip.read(6)
if file_cnt_sig == 'SQLite':
all_files['sqlite'].append(
{'type': 'db', 'file': fileparam})
elif not jfile.endswith('.DS_Store'):
all_files['others'].append(
{'type': 'others', 'file': fileparam})
except Exception:
logger.exception('Getting app files')
return all_files
def send_response(data, api=False):
"""Return JSON Response."""
if api:
return data
return HttpResponse(
json.dumps(data),
content_type='application/json; charset=utf-8')
def invalid_params(api=False):
"""Standard response for invalid params."""
msg = 'Invalid Parameters'
logger.error(msg)
data = {'status': 'failed', 'message': msg}
if api:
return data
return send_response(data)