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

876 lines
51 KiB
Python
Executable File

# -*- coding: utf_8 -*-
# flake8: noqa
"""Module for android manifest analysis."""
import logging
from urllib.parse import urlparse
import requests
from concurrent.futures import ThreadPoolExecutor
from django.utils.html import escape
from mobsf.MobSF.utils import (
append_scan_status,
is_number,
upstream_proxy,
)
from mobsf.MobSF.security import valid_host
from mobsf.StaticAnalyzer.views.android import (
network_security,
)
from mobsf.StaticAnalyzer.views.android.kb import (
android_manifest_desc,
)
logger = logging.getLogger(__name__)
ANDROID_4_2_LEVEL = 17
ANDROID_5_0_LEVEL = 21
ANDROID_8_0_LEVEL = 26
ANDROID_9_0_LEVEL = 28
ANDROID_10_0_LEVEL = 29
ANDROID_MANIFEST_FILE = 'AndroidManifest.xml'
WELL_KNOWN_PATH = '/.well-known/assetlinks.json'
ANDROID_API_LEVEL_MAP = {
'1': '1.0',
'2': '1.1',
'3': '1.5',
'4': '1.6',
'5': '2.0-2.1',
'8': '2.2-2.2.3',
'9': '2.3-2.3.2',
'10': '2.3.3-2.3.7',
'11': '3.0',
'12': '3.1',
'13': '3.2-3.2.6',
'14': '4.0-4.0.2',
'15': '4.0.3-4.0.4',
'16': '4.1-4.1.2',
'17': '4.2-4.2.2',
'18': '4.3-4.3.1',
'19': '4.4-4.4.4',
'20': '4.4W-4.4W.2',
'21': '5.0-5.0.2',
'22': '5.1-5.1.1',
'23': '6.0-6.0.1',
'24': '7.0',
'25': '7.1-7.1.2',
'26': '8.0',
'27': '8.1',
'28': '9',
'29': '10',
'30': '11',
'31': '12',
'32': '12L',
'33': '13',
'34': '14',
'35': '15',
'36': '16',
}
def escape_manifest_attribute(value):
"""Escape manifest attributes."""
if not value:
return value
return escape(value)
def assetlinks_check(act_name, well_knowns):
"""Well known assetlink check."""
findings = []
with ThreadPoolExecutor() as executor:
futures = []
for w_url, host in well_knowns.items():
logger.info(
'App Link Assetlinks Check - [%s] %s', act_name, host)
futures.append(
executor.submit(_check_url, host, w_url))
for future in futures:
findings.append(future.result())
return findings
def _check_url(host, w_url):
"""Check for the presence of Assetlinks URL."""
try:
iden = 'sha256_cert_fingerprints'
proxies, verify = upstream_proxy('https')
status = False
status_code = 0
urls = {w_url}
if w_url.startswith('http://'):
# Upgrade http to https
urls.add(f'https://{w_url[7:]}')
for url in urls:
# Validate the fully-assembled URL — path, port, query, and params.
purl = urlparse(url)
if (purl.path != WELL_KNOWN_PATH
or len(purl.query) > 0
or len(purl.params) > 0):
logger.warning('Invalid Assetlinks URL: %s', url)
continue
if purl.port and purl.port not in (80, 443):
logger.warning(
'Non-standard port in assetlinks URL rejected: %s', url)
continue
r = requests.get(url,
timeout=5,
allow_redirects=False,
proxies=proxies,
verify=verify)
status_code = r.status_code
if (str(status_code).startswith('2') and iden in str(r.json())):
status = True
break
if status_code in (301, 302):
logger.warning('Status Code: [%d], Redirecting to '
'a different URL, skipping check!', status_code)
return {'url': w_url,
'host': host,
'status_code': status_code,
'status': status}
except Exception:
logger.error(f'Well Known Assetlinks Check for URL: {w_url}')
return {'url': w_url,
'host': host,
'status_code': None,
'status': False}
def get_browsable_activities(node, ns):
"""Get Browsable Activities."""
try:
browse_dic = {}
schemes = []
mime_types = []
hosts = []
ports = []
paths = []
path_prefixs = []
path_patterns = []
well_known = {}
catg = node.getElementsByTagName('category')
for cat in catg:
if cat.getAttribute(f'{ns}:name') == 'android.intent.category.BROWSABLE':
data_tag = node.getElementsByTagName('data')
for data in data_tag:
scheme = data.getAttribute(f'{ns}:scheme')
if scheme and scheme not in schemes:
schemes.append(scheme)
mime = data.getAttribute(f'{ns}:mimeType')
if mime and mime not in mime_types:
mime_types.append(mime)
host = data.getAttribute(f'{ns}:host')
if host and host not in hosts:
hosts.append(host)
port = data.getAttribute(f'{ns}:port')
if port and port not in ports:
ports.append(port)
path = data.getAttribute(f'{ns}:path')
if path and path not in paths:
paths.append(path)
path_prefix = data.getAttribute(f'{ns}:pathPrefix')
if path_prefix and path_prefix not in path_prefixs:
path_prefixs.append(path_prefix)
path_pattern = data.getAttribute(f'{ns}:pathPattern')
if path_pattern and path_pattern not in path_patterns:
path_patterns.append(path_pattern)
# Collect possible well-known paths
if (scheme
and scheme in ('http', 'https')
and host
and host != '*'):
host = host.replace('*.', '').replace('#', '')
if not valid_host(host):
logger.warning('Invalid Host: %s', host)
continue
shost = f'{scheme}://{host}'
if port and is_number(port):
if int(port) not in (80, 443):
logger.warning(
'Non-standard port rejected in assetlinks '
'check (port %s bypasses valid_host): %s',
port, host)
continue
c_url = f'{shost}:{port}{WELL_KNOWN_PATH}'
else:
c_url = f'{shost}{WELL_KNOWN_PATH}'
well_known[c_url] = shost
schemes = [scheme + '://' for scheme in schemes]
browse_dic['schemes'] = schemes
browse_dic['mime_types'] = mime_types
browse_dic['hosts'] = hosts
browse_dic['ports'] = ports
browse_dic['paths'] = paths
browse_dic['path_prefixs'] = path_prefixs
browse_dic['path_patterns'] = path_patterns
browse_dic['browsable'] = bool(browse_dic['schemes'])
browse_dic['well_known'] = well_known
return browse_dic
except Exception:
logger.exception('Getting Browsable Activities')
def manifest_analysis(app_dic, man_data_dic):
"""Analyse manifest file."""
# pylint: disable=C0301
checksum = app_dic['md5']
mfxml = app_dic['manifest_parsed_xml']
ns = app_dic['manifest_namespace']
src_type = app_dic['zipped']
app_dir = app_dic['app_dir']
try:
msg = 'Manifest Analysis Started'
logger.info(msg)
append_scan_status(checksum, msg)
exp_count = dict.fromkeys(['act', 'ser', 'bro', 'cnt'], 0)
applications = mfxml.getElementsByTagName('application')
data_tag = mfxml.getElementsByTagName('data')
intents = mfxml.getElementsByTagName('intent-filter')
actions = mfxml.getElementsByTagName('action')
granturipermissions = mfxml.getElementsByTagName(
'grant-uri-permission')
permissions = mfxml.getElementsByTagName('permission')
ret_value = []
ret_list = []
exported = []
browsable_activities = {}
permission_dict = {}
do_netsec = False
debuggable = False
# PERMISSION
for permission in permissions:
if permission.getAttribute(f'{ns}:protectionLevel'):
protectionlevel = permission.getAttribute(
f'{ns}:protectionLevel')
if protectionlevel == '0x00000000':
protectionlevel = 'normal'
elif protectionlevel == '0x00000001':
protectionlevel = 'dangerous'
elif protectionlevel == '0x00000002':
protectionlevel = 'signature'
elif protectionlevel == '0x00000003':
protectionlevel = 'signatureOrSystem'
permission_dict[permission.getAttribute(
f'{ns}:name')] = protectionlevel
elif permission.getAttribute(f'{ns}:name'):
permission_dict[permission.getAttribute(
f'{ns}:name')] = 'normal'
# GENERAL
if man_data_dic['min_sdk'] and int(man_data_dic['min_sdk']) < ANDROID_8_0_LEVEL:
minsdk = man_data_dic.get('min_sdk')
android_version = ANDROID_API_LEVEL_MAP.get(minsdk, 'XX')
ret_list.append(('vulnerable_os_version', (android_version, minsdk,), ()))
elif man_data_dic['min_sdk'] and int(man_data_dic['min_sdk']) < ANDROID_10_0_LEVEL:
minsdk = man_data_dic.get('min_sdk')
android_version = ANDROID_API_LEVEL_MAP.get(minsdk, 'XX')
ret_list.append(('vulnerable_os_version2', (android_version, minsdk,), ()))
# APPLICATIONS
# Handle multiple application tags in AAR
backupDisabled = False
for application in applications:
# Esteve 23.07.2016 - begin - identify permission at the
# application level
if application.getAttribute(f'{ns}:permission'):
perm_appl_level_exists = True
perm_appl_level = application.getAttribute(
f'{ns}:permission')
else:
perm_appl_level_exists = False
# End
if application.getAttribute(f'{ns}:usesCleartextTraffic') == 'true':
ret_list.append(('clear_text_traffic', (), ()))
if application.getAttribute(f'{ns}:directBootAware') == 'true':
ret_list.append(('direct_boot_aware', (), ()))
if application.getAttribute(f'{ns}:networkSecurityConfig'):
item = application.getAttribute(f'{ns}:networkSecurityConfig')
ret_list.append(('has_network_security', (item,), ()))
do_netsec = item
if application.getAttribute(f'{ns}:debuggable') == 'true':
ret_list.append(('app_is_debuggable', (), ()))
debuggable = True
if application.getAttribute(f'{ns}:allowBackup') == 'true':
ret_list.append(('app_allowbackup', (), ()))
elif application.getAttribute(f'{ns}:allowBackup') == 'false':
backupDisabled = True
else:
if not backupDisabled:
ret_list.append(('allowbackup_not_set', (), ()))
if application.getAttribute(f'{ns}:testOnly') == 'true':
ret_list.append(('app_in_test_mode', (), ()))
for node in application.childNodes:
an_or_a = ''
if node.nodeName == 'activity':
itemname = 'Activity'
cnt_id = 'act'
an_or_a = 'n'
elif node.nodeName == 'activity-alias':
itemname = 'Activity-Alias'
cnt_id = 'act'
an_or_a = 'n'
elif node.nodeName == 'provider':
itemname = 'Content Provider'
cnt_id = 'cnt'
elif node.nodeName == 'receiver':
itemname = 'Broadcast Receiver'
cnt_id = 'bro'
elif node.nodeName == 'service':
itemname = 'Service'
cnt_id = 'ser'
else:
itemname = 'NIL'
item = ''
# Checks for Activities
if itemname in ['Activity', 'Activity-Alias']:
item = node.getAttribute(f'{ns}:name')
item = escape_manifest_attribute(item)
# Browsable Activities
browse_dic = get_browsable_activities(node, ns)
if browse_dic['browsable']:
browsable_activities[node.getAttribute(
f'{ns}:name')] = browse_dic
for finding in assetlinks_check(item, browse_dic['well_known']):
if not finding['status']:
ret_list.append(('well_known_assetlinks',
(item, finding['host']),
(finding['url'],
finding['status_code'])))
# Task Affinity
task_affinity = node.getAttribute(f'{ns}:taskAffinity')
if (task_affinity):
ret_list.append(('task_affinity_set', (item,), ()))
# LaunchMode
try:
affected_sdk = int(
man_data_dic['min_sdk']) < ANDROID_5_0_LEVEL
except Exception:
# in case min_sdk is not defined we assume vulnerability
affected_sdk = True
launchmode = node.getAttribute(f'{ns}:launchMode')
modes = ('singleTask', 'singleInstance')
if (affected_sdk
and launchmode in modes):
ret_list.append(('non_standard_launchmode', (item,), ()))
# Android Task Hijacking or StrandHogg 1.0
try:
target_sdk = int(man_data_dic['target_sdk'])
except Exception:
try:
target_sdk = int(man_data_dic['min_sdk'])
except Exception:
target_sdk = ANDROID_8_0_LEVEL
if (target_sdk < ANDROID_9_0_LEVEL
and launchmode == 'singleTask'):
ret_list.append(('task_hijacking', (item,), (target_sdk,)))
# Android StrandHogg 2.0
exported_act = node.getAttribute(f'{ns}:exported')
if (target_sdk < ANDROID_10_0_LEVEL
and exported_act == 'true'
and (launchmode != 'singleInstance' or task_affinity != '')):
ret_list.append(('task_hijacking2', (item,), (target_sdk,)))
# Exported Check
item = ''
is_inf = False
is_perm_exist = False
# Esteve 23.07.2016 - begin - initialise variables to identify
# the existence of a permission at the component level that
# matches a permission at the manifest level
prot_level_exist = False
protlevel = ''
# End
if itemname != 'NIL':
if node.getAttribute(f'{ns}:exported') == 'true':
perm = ''
item = node.getAttribute(f'{ns}:name')
item = escape_manifest_attribute(item)
if node.getAttribute(f'{ns}:permission'):
# permission exists
perm = ('<strong>Permission: </strong>'
+ node.getAttribute(f'{ns}:permission'))
is_perm_exist = True
if item != man_data_dic['mainactivity']:
if is_perm_exist:
prot = ''
if node.getAttribute(f'{ns}:permission') in permission_dict:
prot = ('</br><strong>protectionLevel: </strong>'
+ permission_dict[node.getAttribute(f'{ns}:permission')])
# Esteve 23.07.2016 - begin - take into account protection level of the permission when claiming that a component is protected by it;
# - the permission might not be defined in the application being analysed, if so, the protection level is not known;
# - activities (or activity-alias) that are exported and have an unknown or normal or dangerous protection level are
# included in the EXPORTED data structure for further treatment; components in this situation are also
# counted as exported.
prot_level_exist = True
protlevel = permission_dict[
node.getAttribute(f'{ns}:permission')]
if prot_level_exist:
if protlevel == 'normal':
ret_list.append(
('exported_protected_permission_normal', (itemname, item, perm + prot), (an_or_a, itemname)))
if itemname in ['Activity', 'Activity-Alias']:
exported.append(item)
exp_count[cnt_id] = exp_count[
cnt_id] + 1
elif protlevel == 'dangerous':
ret_list.append(
('exported_protected_permission_dangerous', (itemname, item, perm + prot), (an_or_a, itemname)))
if itemname in ['Activity', 'Activity-Alias']:
exported.append(item)
exp_count[cnt_id] = exp_count[
cnt_id] + 1
elif protlevel == 'signature':
ret_list.append(
('exported_protected_permission_signature', (itemname, item, perm + prot), (an_or_a, itemname)))
elif protlevel == 'signatureOrSystem':
ret_list.append(
('exported_protected_permission_signatureorsystem', (itemname, item, perm + prot), (an_or_a, itemname)))
else:
ret_list.append(
('exported_protected_permission_not_defined', (itemname, item, perm), (an_or_a, itemname)))
if itemname in ['Activity', 'Activity-Alias']:
exported.append(item)
exp_count[cnt_id] = exp_count[cnt_id] + 1
# Esteve 23.07.2016 - end
else:
# Esteve 24.07.2016 - begin - At this point, we are dealing with components that do not have a permission neither at the component level nor at the
# application level. As they are exported, they
# are not protected.
if perm_appl_level_exists is False:
ret_list.append(
('explicitly_exported', (itemname, item), (an_or_a, itemname)))
if itemname in ['Activity', 'Activity-Alias']:
exported.append(item)
exp_count[cnt_id] = exp_count[cnt_id] + 1
# Esteve 24.07.2016 - end
# Esteve 24.07.2016 - begin - At this point, we are dealing with components that have a permission at the application level, but not at the component
# level. Two options are possible:
# 1) The permission is defined at the manifest level, which allows us to differentiate the level of protection as
# we did just above for permissions specified at the component level.
# 2) The permission is not defined at the manifest level, which means the protection level is unknown, as it is not
# defined in the analysed application.
else:
perm = '<strong>Permission: </strong>' + perm_appl_level
prot = ''
if perm_appl_level in permission_dict:
prot = ('</br><strong>protectionLevel: </strong>'
+ permission_dict[perm_appl_level])
prot_level_exist = True
protlevel = permission_dict[
perm_appl_level]
if prot_level_exist:
if protlevel == 'normal':
ret_list.append(
('exported_protected_permission_normal_app_level', (itemname, item, perm + prot), (an_or_a, itemname)))
if itemname in ['Activity', 'Activity-Alias']:
exported.append(item)
exp_count[cnt_id] = exp_count[
cnt_id] + 1
elif protlevel == 'dangerous':
ret_list.append(
('exported_protected_permission_dangerous_app_level', (itemname, item, perm + prot), (an_or_a, itemname)))
if itemname in ['Activity', 'Activity-Alias']:
exported.append(item)
exp_count[cnt_id] = exp_count[
cnt_id] + 1
elif protlevel == 'signature':
ret_list.append(
('exported_protected_permission', (itemname, item, perm + prot), (an_or_a, itemname)))
elif protlevel == 'signatureOrSystem':
ret_list.append(
('exported_protected_permission_signatureorsystem_app_level', (itemname, item, perm + prot), (an_or_a, itemname)))
else:
ret_list.append(
('exported_protected_permission_app_level', (itemname, item, perm), (an_or_a, itemname)))
if itemname in ['Activity', 'Activity-Alias']:
exported.append(item)
exp_count[cnt_id] = exp_count[
cnt_id] + 1
# Esteve 24.07.2016 - end
elif node.getAttribute(f'{ns}:exported') != 'false':
# Check for Implicitly Exported
# Logic to support intent-filter
intentfilters = node.childNodes
for i in intentfilters:
inf = i.nodeName
if inf == 'intent-filter':
is_inf = True
if is_inf:
item = node.getAttribute(f'{ns}:name')
item = escape_manifest_attribute(item)
if node.getAttribute(f'{ns}:permission'):
# permission exists
perm = ('<strong>Permission: </strong>'
+ node.getAttribute(f'{ns}:permission'))
is_perm_exist = True
if item != man_data_dic['mainactivity']:
if is_perm_exist:
prot = ''
if node.getAttribute(f'{ns}:permission') in permission_dict:
prot = ('</br><strong>protectionLevel: </strong>'
+ permission_dict[node.getAttribute(f'{ns}:permission')])
# Esteve 24.07.2016 - begin - take into account protection level of the permission when claiming that a component is protected by it;
# - the permission might not be defined in the application being analysed, if so, the protection level is not known;
# - activities (or activity-alias) that are exported and have an unknown or normal or dangerous protection level are
# included in the EXPORTED data structure for further treatment; components in this situation are also
# counted as exported.
prot_level_exist = True
protlevel = permission_dict[
node.getAttribute(f'{ns}:permission')]
if prot_level_exist:
if protlevel == 'normal':
ret_list.append(
('exported_protected_permission_normal', (itemname, item, perm + prot), (an_or_a, itemname)))
if itemname in ['Activity', 'Activity-Alias']:
exported.append(item)
exp_count[cnt_id] = exp_count[
cnt_id] + 1
elif protlevel == 'dangerous':
ret_list.append(
('exported_protected_permission_dangerous', (itemname, item, perm + prot), (an_or_a, itemname)))
if itemname in ['Activity', 'Activity-Alias']:
exported.append(item)
exp_count[cnt_id] = exp_count[
cnt_id] + 1
elif protlevel == 'signature':
ret_list.append(
('exported_protected_permission_signature', (itemname, item, perm + prot), (an_or_a, itemname)))
elif protlevel == 'signatureOrSystem':
ret_list.append(
('exported_protected_permission_signatureorsystem', (itemname, item, perm + prot), (an_or_a, itemname)))
else:
ret_list.append(
('exported_protected_permission_not_defined', (itemname, item, perm), (an_or_a, itemname)))
if itemname in ['Activity', 'Activity-Alias']:
exported.append(item)
exp_count[cnt_id] = exp_count[
cnt_id] + 1
# Esteve 24.07.2016 - end
else:
# Esteve 24.07.2016 - begin - At this point, we are dealing with components that do not have a permission neither at the component level nor at the
# application level. As they are exported,
# they are not protected.
if perm_appl_level_exists is False:
ret_list.append(
('exported_intent_filter_exists', (itemname, item), (an_or_a, itemname, itemname)))
if itemname in ['Activity', 'Activity-Alias']:
exported.append(item)
exp_count[cnt_id] = exp_count[
cnt_id] + 1
# Esteve 24.07.2016 - end
# Esteve 24.07.2016 - begin - At this point, we are dealing with components that have a permission at the application level, but not at the component
# level. Two options are possible:
# 1) The permission is defined at the manifest level, which allows us to differentiate the level of protection as
# we did just above for permissions specified at the component level.
# 2) The permission is not defined at the manifest level, which means the protection level is unknown, as it is not
# defined in the analysed application.
else:
perm = '<strong>Permission: </strong>' + perm_appl_level
prot = ''
if perm_appl_level in permission_dict:
prot = ('</br><strong>protectionLevel: </strong>'
+ permission_dict[perm_appl_level])
prot_level_exist = True
protlevel = permission_dict[
perm_appl_level]
if prot_level_exist:
if protlevel == 'normal':
ret_list.append(
('exported_protected_permission_normal_app_level', (itemname, item, perm + prot), (an_or_a, itemname)))
if itemname in ['Activity', 'Activity-Alias']:
exported.append(item)
exp_count[cnt_id] = exp_count[
cnt_id] + 1
elif protlevel == 'dangerous':
ret_list.append(
('exported_protected_permission_dangerous_app_level', (itemname, item, perm + prot), (an_or_a, itemname)))
if itemname in ['Activity', 'Activity-Alias']:
exported.append(item)
exp_count[cnt_id] = exp_count[
cnt_id] + 1
elif protlevel == 'signature':
ret_list.append(
('exported_protected_permission', (itemname, item, perm + prot), (an_or_a, itemname)))
elif protlevel == 'signatureOrSystem':
ret_list.append(
('exported_protected_permission_signatureorsystem_app_level', (itemname, item, perm + prot), (an_or_a, itemname)))
else:
ret_list.append(
('exported_protected_permission_app_level', (itemname, item, perm), (an_or_a, itemname)))
if itemname in ['Activity', 'Activity-Alias']:
exported.append(item)
exp_count[cnt_id] = exp_count[
cnt_id] + 1
# Esteve 24.07.2016 - end
# Esteve 29.07.2016 - begin The component is not explicitly exported (android:exported is not 'true'). It is not implicitly exported either (it does not
# make use of an intent filter). Despite that, it could still be exported by default, if it is a content provider and the android:targetSdkVersion
# is older than 17 (Jelly Bean, Android version 4.2). This is true regardless of the system's API level.
# Finally, it must also be taken into account that, if the minSdkVersion is greater or equal than 17, this check is unnecessary, because the
# app will not be run on a system where the
# system's API level is below 17.
else:
if man_data_dic['min_sdk'] and man_data_dic['target_sdk'] and int(man_data_dic['min_sdk']) < ANDROID_4_2_LEVEL:
if itemname == 'Content Provider' and int(man_data_dic['target_sdk']) < ANDROID_4_2_LEVEL:
perm = ''
item = node.getAttribute(f'{ns}:name')
item = escape_manifest_attribute(item)
if node.getAttribute(f'{ns}:permission'):
# permission exists
perm = ('<strong>Permission: </strong>'
+ node.getAttribute(f'{ns}:permission'))
is_perm_exist = True
if is_perm_exist:
prot = ''
if node.getAttribute(f'{ns}:permission') in permission_dict:
prot = ('</br><strong>protectionLevel: </strong>'
+ permission_dict[node.getAttribute(f'{ns}:permission')])
prot_level_exist = True
protlevel = permission_dict[
node.getAttribute(f'{ns}:permission')]
if prot_level_exist:
if protlevel == 'normal':
ret_list.append(
('exported_provider_normal', (itemname, item, perm + prot), (an_or_a, itemname)))
exp_count[cnt_id] = exp_count[
cnt_id] + 1
elif protlevel == 'dangerous':
ret_list.append(
('exported_provider_danger', (itemname, item, perm + prot), (an_or_a, itemname)))
exp_count[cnt_id] = exp_count[
cnt_id] + 1
elif protlevel == 'signature':
ret_list.append(
('exported_provider_signature', (itemname, item, perm + prot), (an_or_a, itemname)))
elif protlevel == 'signatureOrSystem':
ret_list.append(
('exported_provider_signatureorsystem', (itemname, item, perm + prot), (an_or_a, itemname)))
else:
ret_list.append(
('exported_provider_unknown', (itemname, item, perm), (an_or_a, itemname)))
exp_count[cnt_id] = exp_count[
cnt_id] + 1
else:
if perm_appl_level_exists is False:
ret_list.append(
('exported_provider', (itemname, item), (an_or_a, itemname)))
exp_count[cnt_id] = exp_count[
cnt_id] + 1
else:
perm = '<strong>Permission: </strong>' + perm_appl_level
prot = ''
if perm_appl_level in permission_dict:
prot = ('</br><strong>protectionLevel: </strong>'
+ permission_dict[perm_appl_level])
prot_level_exist = True
protlevel = permission_dict[
perm_appl_level]
if prot_level_exist:
if protlevel == 'normal':
ret_list.append(
('exported_provider_normal_app', (itemname, item, perm + prot), (an_or_a, itemname)))
exp_count[cnt_id] = exp_count[
cnt_id] + 1
elif protlevel == 'dangerous':
ret_list.append(
('exported_provider_danger_appl', (itemname, item, perm + prot), (an_or_a, itemname)))
exp_count[cnt_id] = exp_count[
cnt_id] + 1
elif protlevel == 'signature':
ret_list.append(
('exported_provider_signature_appl', (itemname, item, perm + prot), (an_or_a, itemname)))
elif protlevel == 'signatureOrSystem':
ret_list.append(
('exported_provider_signatureorsystem_app', (itemname, item, perm + prot), (an_or_a, itemname)))
else:
ret_list.append(
('exported_provider_unknown_app', (itemname, item, perm), (an_or_a, itemname)))
exp_count[cnt_id] = exp_count[
cnt_id] + 1
# Esteve 29.07.2016 - end
# Esteve 08.08.2016 - begin - If the content provider does not target an API version lower than 17, it could still be exported by default, depending
# on the API version of the platform. If it was below 17, the content
# provider would be exported by default.
else:
if itemname == 'Content Provider' and int(man_data_dic['target_sdk']) >= 17:
perm = ''
item = node.getAttribute(
f'{ns}:name')
item = escape_manifest_attribute(item)
if node.getAttribute(f'{ns}:permission'):
# permission exists
perm = ('<strong>Permission: </strong>'
+ node.getAttribute(f'{ns}:permission'))
is_perm_exist = True
if is_perm_exist:
prot = ''
if node.getAttribute(f'{ns}:permission') in permission_dict:
prot = ('</br><strong>protectionLevel: </strong>'
+ permission_dict[node.getAttribute(f'{ns}:permission')])
prot_level_exist = True
protlevel = permission_dict[
node.getAttribute(f'{ns}:permission')]
if prot_level_exist:
if protlevel == 'normal':
ret_list.append(
('exported_provider_normal_new', (itemname, item, perm + prot), (itemname)))
exp_count[cnt_id] = exp_count[
cnt_id] + 1
if protlevel == 'dangerous':
ret_list.append(
('exported_provider_danger_new', (itemname, item, perm + prot), (itemname)))
exp_count[cnt_id] = exp_count[
cnt_id] + 1
if protlevel == 'signature':
ret_list.append(
('exported_provider_signature_new', (itemname, item, perm + prot), (itemname)))
if protlevel == 'signatureOrSystem':
ret_list.append(
('exported_provider_signatureorsystem_new', (itemname, item, perm + prot), (an_or_a, itemname)))
else:
ret_list.append(
('exported_provider_unknown_new', (itemname, item, perm), (itemname)))
exp_count[cnt_id] = exp_count[
cnt_id] + 1
else:
if perm_appl_level_exists is False:
ret_list.append(
('exported_provider_2', (itemname, item), (an_or_a, itemname)))
exp_count[cnt_id] = exp_count[
cnt_id] + 1
else:
perm = '<strong>Permission: </strong>' + perm_appl_level
prot = ''
if perm_appl_level in permission_dict:
prot = ('</br><strong>protectionLevel: </strong>'
+ permission_dict[perm_appl_level])
prot_level_exist = True
protlevel = permission_dict[
perm_appl_level]
if prot_level_exist:
if protlevel == 'normal':
ret_list.append(
('exported_provider_normal_app_new', (itemname, item, perm + prot), (an_or_a, itemname)))
exp_count[cnt_id] = exp_count[
cnt_id] + 1
elif protlevel == 'dangerous':
ret_list.append(
('exported_provider_danger_app_new', (itemname, item, perm + prot), (an_or_a, itemname)))
exp_count[cnt_id] = exp_count[
cnt_id] + 1
elif protlevel == 'signature':
ret_list.append(
('exported_provider_signature_app_new', (itemname, item, perm + prot), (an_or_a, itemname)))
elif protlevel == 'signatureOrSystem':
ret_list.append(
('exported_provider_signatureorsystem_app_new', (itemname, item, perm + prot), (an_or_a, itemname)))
else:
ret_list.append(
('exported_provider_unknown_app_new', (itemname, item, perm), (an_or_a, itemname)))
exp_count[cnt_id] = exp_count[
cnt_id] + 1
# Esteve 08.08.2016 - end
# GRANT-URI-PERMISSIONS
for granturi in granturipermissions:
if granturi.getAttribute(f'{ns}:pathPrefix') == '/':
ret_list.append(
('improper_provider_permission', ('pathPrefix=/',), ()))
elif granturi.getAttribute(f'{ns}:path') == '/':
ret_list.append(('improper_provider_permission', ('path=/',), ()))
elif granturi.getAttribute(f'{ns}:pathPattern') == '*':
ret_list.append(('improper_provider_permission', ('path=*',), ()))
# DATA
for data in data_tag:
if data.getAttribute(f'{ns}:scheme') == 'android_secret_code':
xmlhost = data.getAttribute(f'{ns}:host')
xmlhost = escape_manifest_attribute(xmlhost)
ret_list.append(('dialer_code_found', (xmlhost,), ()))
elif data.getAttribute(f'{ns}:port'):
dataport = data.getAttribute(f'{ns}:port')
dataport = escape_manifest_attribute(dataport)
ret_list.append(('sms_receiver_port_found', (dataport,), ()))
# INTENTS
processed_priorities = {}
for intent in intents:
if intent.getAttribute(f'{ns}:priority').isdigit():
value = intent.getAttribute(f'{ns}:priority')
if int(value) > 100:
if value not in processed_priorities:
processed_priorities[value] = 1
else:
processed_priorities[value] += 1
for priority, count in processed_priorities.items():
ret_list.append(
('high_intent_priority_found', (priority, count,), ()))
# ACTIONS
for action in actions:
if action.getAttribute(f'{ns}:priority').isdigit():
value = action.getAttribute(f'{ns}:priority')
if int(value) > 100:
ret_list.append(
('high_action_priority_found', (value,), ()))
for a_key, t_name, t_desc in ret_list:
a_template = android_manifest_desc.MANIFEST_DESC.get(a_key)
if a_template:
ret_value.append({
'rule': a_key,
'title': a_template['title'] % t_name,
'severity': a_template['level'],
'description': a_template['description'] % t_desc,
'name': a_template['name'] % t_name,
'component': t_name,
})
else:
logger.warning("No template found for key '%s'", a_key)
for category in man_data_dic['categories']:
if category == 'android.intent.category.LAUNCHER':
break
permissions = {}
for k, permission in man_data_dic['perm'].items():
permissions[k] = (
{
'status': permission[0],
'info': permission[1],
'description': permission[2],
})
# Prepare return dict
exported_comp = {
'exported_activities': exp_count['act'],
'exported_services': exp_count['ser'],
'exported_receivers': exp_count['bro'],
'exported_providers': exp_count['cnt'],
}
man_an_dic = {
'manifest_anal': ret_value,
'exported_act': exported,
'exported_cnt': exported_comp,
'browsable_activities': browsable_activities,
'permissions': permissions,
'network_security': network_security.analysis(
checksum,
app_dir,
do_netsec,
debuggable,
src_type),
}
return man_an_dic
except Exception as exp:
msg = 'Error Performing Manifest Analysis'
logger.exception(msg)
append_scan_status(checksum, msg, repr(exp))