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:
Vorsynth
2026-09-20 18:04:55 -07:00
committed by GitHub
co-authored by Ajin Abraham Cursor Agent
parent 2a8399fac9
commit f57a3c997c
5 changed files with 80 additions and 19 deletions
+4 -3
View File
@@ -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
+1
View File
@@ -63,6 +63,7 @@ class AndroidChecks(forms.Form):
('studio', 'studio'),
('java', 'java'),
('smali', 'smali'),
('xml', 'xml'),
('apk', 'apk'),
('jar', 'jar'),
('aar', 'aar'),
+3
View File
@@ -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)
+9 -1
View File
@@ -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&amp;type=ipa&amp;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&amp;type=xml&amp;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&amp;type=ipa&amp;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&amp;type=xml&amp;file=AndroidManifest.xml" -H "X-Mobsf-Api-Key: {{ api_key}}"
</code></pre>
</li>
</ul>
</li>
</ul>