mirror of
https://github.com/MobSF/Mobile-Security-Framework-MobSF.git
synced 2026-10-12 02:06:59 +00:00
feat: expose decompiled Android XML files via the view_source REST API (#2659)
* feat: expose decompiled Android XML files via the view_source REST API AndroidManifest.xml and other resource XML files are already decompiled to apktool_out/ during static analysis, but there was no way to fetch them through the API - only Java/Kotlin/smali sources were reachable via view_source, and the standalone manifest_view page is web-only with no API route. Add an 'xml' type to view_source that points at apktool_out/, and register it in the API dispatcher's Android type set (it also covers eclipse/studio/apk/java/smali there; xml needs to be added explicitly or it falls through to the iOS handler). .xml was already in the allowed file extension list, so no other validation changes were needed. Path traversal protection (is_safe_path) is unchanged and applies to the new type like every other one. Co-authored-by: alanhasn * fix: safely support XML across Android scan types Co-authored-by: Ajin Abraham <[email protected]> * fix: validate view source API parameters Co-authored-by: Ajin Abraham <[email protected]> * Remove ViewSourceXMLTest Co-authored-by: Ajin Abraham <[email protected]> * fix: address CodeQL findings on Android view_source Use validated form cleaned_data for path construction, keep exception details out of JSON error responses, and annotate post-validation path checks as known CodeQL false positives. Co-authored-by: Ajin Abraham <[email protected]> --------- Co-authored-by: Ajin Abraham <[email protected]> Co-authored-by: Cursor Agent <[email protected]>
This commit is contained in:
co-authored by
Ajin Abraham
Cursor Agent
parent
2a8399fac9
commit
f57a3c997c
@@ -214,16 +214,17 @@ def api_search(request):
|
||||
def api_view_source(request):
|
||||
"""View Source for android & ios source file."""
|
||||
params = {'file', 'type', 'hash'}
|
||||
if set(request.POST) < params:
|
||||
if not params.issubset(request.POST):
|
||||
return make_api_response(
|
||||
{'error': 'Missing Parameters'}, 422)
|
||||
if request.POST['type'] in {'eclipse', 'studio',
|
||||
'apk', 'java', 'smali'}:
|
||||
'apk', 'java', 'smali', 'xml'}:
|
||||
resp = view_source.run(request, api=True)
|
||||
else:
|
||||
resp = ios_view_source.run(request, api=True)
|
||||
status = resp.pop('_status_code', None)
|
||||
if 'error' in resp:
|
||||
response = make_api_response(resp, 500)
|
||||
response = make_api_response(resp, status or 400)
|
||||
else:
|
||||
response = make_api_response(resp, 200)
|
||||
return response
|
||||
|
||||
@@ -63,6 +63,7 @@ class AndroidChecks(forms.Form):
|
||||
('studio', 'studio'),
|
||||
('java', 'java'),
|
||||
('smali', 'smali'),
|
||||
('xml', 'xml'),
|
||||
('apk', 'apk'),
|
||||
('jar', 'jar'),
|
||||
('aar', 'aar'),
|
||||
|
||||
@@ -353,6 +353,9 @@ def api_test():
|
||||
files = [{'file': 'jakhar/aseem/diva/MainActivity.java',
|
||||
'type': 'apk',
|
||||
'hash': '82ab8b2193b3cfb1c737e3a786be363a'},
|
||||
{'file': 'AndroidManifest.xml',
|
||||
'type': 'xml',
|
||||
'hash': '82ab8b2193b3cfb1c737e3a786be363a'},
|
||||
{'file': 'opensecurity/webviewignoressl/MainActivity.java',
|
||||
'type': 'studio',
|
||||
'hash': '52c50ae824e329ba8b5b7a0f523efffe'},
|
||||
|
||||
@@ -10,10 +10,17 @@ from django.utils.html import escape
|
||||
from django.http import JsonResponse
|
||||
|
||||
from mobsf.MobSF.forms import FormUtil
|
||||
from mobsf.MobSF.security import is_safe_path
|
||||
from mobsf.MobSF.security import (
|
||||
is_pipe_or_link,
|
||||
is_safe_path,
|
||||
)
|
||||
from mobsf.MobSF.utils import (
|
||||
print_n_send_error_response,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.models import (
|
||||
RecentScansDB,
|
||||
StaticAnalyzerAndroid,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.shared_func import (
|
||||
find_java_source_folder,
|
||||
)
|
||||
@@ -32,17 +39,42 @@ def send_json(data):
|
||||
return JsonResponse(data, safe=False)
|
||||
|
||||
|
||||
def send_error(request, err, api_mode, json_resp, exp=None):
|
||||
def send_error(request, err, api_mode, json_resp, exp=None, status=400):
|
||||
"""Send error message as dict or JSON."""
|
||||
if json_resp:
|
||||
res = print_n_send_error_response(request, err, True)
|
||||
return JsonResponse(res, safe=False, status=status)
|
||||
if exp:
|
||||
res = print_n_send_error_response(request, err, api_mode, exp)
|
||||
else:
|
||||
res = print_n_send_error_response(request, err, api_mode)
|
||||
if json_resp:
|
||||
return send_json(res)
|
||||
if api_mode:
|
||||
res['_status_code'] = status
|
||||
else:
|
||||
res.status_code = status
|
||||
return res
|
||||
|
||||
|
||||
def get_xml_root(base, checksum):
|
||||
"""Get the XML root from validated scan metadata."""
|
||||
scan = RecentScansDB.objects.filter(MD5=checksum).first()
|
||||
analysis = StaticAnalyzerAndroid.objects.filter(MD5=checksum).first()
|
||||
if not scan or not analysis:
|
||||
return None
|
||||
|
||||
scan_type = scan.SCAN_TYPE
|
||||
app_type = analysis.APP_TYPE
|
||||
if app_type == 'apk' and scan_type in {'apk', 'aab', 'apks', 'xapk'}:
|
||||
return base / 'apktool_out'
|
||||
if app_type == 'aar' and scan_type == 'aar':
|
||||
return base
|
||||
if app_type == 'studio' and scan_type == 'zip':
|
||||
return base / 'app' / 'src' / 'main'
|
||||
if app_type == 'eclipse' and scan_type == 'zip':
|
||||
return base
|
||||
return None
|
||||
|
||||
|
||||
@login_required
|
||||
def run(request, api=False):
|
||||
"""View the source of a file."""
|
||||
@@ -52,22 +84,26 @@ def run(request, api=False):
|
||||
logger.info('View Java Source File')
|
||||
exp = 'Error Description'
|
||||
if api:
|
||||
fil = request.POST['file']
|
||||
md5 = request.POST['hash']
|
||||
typ = request.POST['type']
|
||||
viewsource_form = ViewSourceAndroidApiForm(request.POST)
|
||||
else:
|
||||
fil = request.GET['file']
|
||||
md5 = request.GET['md5']
|
||||
typ = request.GET['type']
|
||||
viewsource_form = ViewSourceAndroidForm(request.GET)
|
||||
if not viewsource_form.is_valid():
|
||||
err = FormUtil.errors_message(viewsource_form)
|
||||
return send_error(request, err, api_mode, json_resp, exp)
|
||||
cleaned = viewsource_form.cleaned_data
|
||||
fil = cleaned['file']
|
||||
typ = cleaned['type']
|
||||
md5 = cleaned['hash'] if api else cleaned['md5']
|
||||
base = Path(settings.UPLD_DIR) / md5
|
||||
if typ == 'smali':
|
||||
src = base / 'smali_source'
|
||||
syntax = 'smali'
|
||||
elif typ == 'xml':
|
||||
src = get_xml_root(base, md5)
|
||||
if src is None:
|
||||
msg = 'XML viewing is not supported for this scan type'
|
||||
return send_error(request, msg, api_mode, json_resp)
|
||||
syntax = 'xml'
|
||||
else:
|
||||
try:
|
||||
src, syntax, _ = find_java_source_folder(base)
|
||||
@@ -75,14 +111,27 @@ def run(request, api=False):
|
||||
msg = 'Invalid directory or file extension'
|
||||
return send_error(request, msg, api_mode, json_resp)
|
||||
|
||||
if (not src.exists() # lgtm [py/path-injection]
|
||||
or not src.is_dir() # lgtm [py/path-injection]
|
||||
or is_pipe_or_link(src)):
|
||||
msg = 'Source directory not found'
|
||||
return send_error(
|
||||
request, msg, api_mode, json_resp, status=404)
|
||||
sfile = src / fil
|
||||
if not is_safe_path(src, sfile.as_posix(), fil):
|
||||
msg = 'Path Traversal Detected!'
|
||||
return send_error(request, msg, api_mode, json_resp)
|
||||
if (not sfile.exists() # lgtm [py/path-injection]
|
||||
or not sfile.is_file() # lgtm [py/path-injection]
|
||||
or is_pipe_or_link(sfile)):
|
||||
msg = 'Source file not found'
|
||||
return send_error(
|
||||
request, msg, api_mode, json_resp, status=404)
|
||||
context = {
|
||||
'title': escape(ntpath.basename(fil)),
|
||||
'file': escape(ntpath.basename(fil)),
|
||||
'data': sfile.read_text('utf-8', 'ignore'),
|
||||
'data': sfile.read_text( # lgtm [py/path-injection]
|
||||
'utf-8', 'ignore'),
|
||||
'type': syntax,
|
||||
'sqlite': {},
|
||||
'version': settings.MOBSF_VER,
|
||||
@@ -93,8 +142,7 @@ def run(request, api=False):
|
||||
if api_mode:
|
||||
return context
|
||||
return render(request, template, context)
|
||||
except Exception as exp:
|
||||
except Exception:
|
||||
logger.exception('Error Viewing Source')
|
||||
msg = str(exp)
|
||||
exp = exp.__doc__
|
||||
return send_error(request, msg, api_mode, json_resp, exp)
|
||||
return send_error(
|
||||
request, 'Error Viewing Source', api_mode, json_resp)
|
||||
|
||||
@@ -820,7 +820,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>apk/ipa/studio/eclipse/ios</td>
|
||||
<td>apk/ipa/studio/eclipse/ios/xml</td>
|
||||
<td>Yes</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -866,6 +866,10 @@
|
||||
<pre><code>curl -X POST --url http://localhost:8000/api/v1/view_source --data "hash=6c23c2970551be15f32bbab0b5db0c71&type=ipa&file=classdump.txt" -H "Authorization: {{ api_key}}"
|
||||
</code></pre>
|
||||
</li>
|
||||
<li>
|
||||
<pre><code>curl -X POST --url http://localhost:8000/api/v1/view_source --data "hash=18e244926da1e49c5b8ffc1c30de8abc&type=xml&file=AndroidManifest.xml" -H "Authorization: {{ api_key}}"
|
||||
</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
<p>OR</p>
|
||||
<ul>
|
||||
@@ -877,6 +881,10 @@
|
||||
<pre><code>curl -X POST --url http://localhost:8000/api/v1/view_source --data "hash=6c23c2970551be15f32bbab0b5db0c71&type=ipa&file=classdump.txt" -H "X-Mobsf-Api-Key: {{ api_key}}"
|
||||
</code></pre>
|
||||
</li>
|
||||
<li>
|
||||
<pre><code>curl -X POST --url http://localhost:8000/api/v1/view_source --data "hash=18e244926da1e49c5b8ffc1c30de8abc&type=xml&file=AndroidManifest.xml" -H "X-Mobsf-Api-Key: {{ api_key}}"
|
||||
</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
Reference in New Issue
Block a user