Fix CSRF in dynamic analysis actions (#2679)

* Harden path traversal checks for Windows root-relative escapes.

Reject POSIX/Windows absolute paths in is_path_traversal and require post-join containment at icon, network-config, and archive extraction sinks.

Co-authored-by: Cursor <[email protected]>

* Restrict network security config names to a basename.

CodeQL flagged the manifest-derived join; reduce the resource name first so path construction cannot escape xml_dir.

Co-authored-by: Cursor <[email protected]>

* Use os.path.basename for CodeQL-recognized path sanitization.

Path.name did not clear py/path-injection taint; basename plus existing containment checks keep the network-config read inside xml_dir.

Co-authored-by: Cursor <[email protected]>

* Drop CodeQL-appeasement basename sanitization from network config reads.

is_safe_path after join already rejects traversal, absolute, and Windows root-relative names; CodeQL py/path-injection is a false positive.

Co-authored-by: Cursor <[email protected]>

* Bound androguard ZIP member decompression

Use the configured per-file ZIP limit for apkinspector reads and cap
actual raw-DEFLATE output. Keep decompression-limit failures out of the
tampered-entry fallback so malicious compressed data cannot be treated
as stored bytes.

Co-authored-by: Cursor <[email protected]>

* Reject inactive SAML users before ACS group updates.

Existing accounts mapped from a valid IdP assertion should not have groups replaced or be passed to login() when is_active is False.

Co-authored-by: Cursor <[email protected]>

* Return the ACS error page for inactive users instead of raising Exception.

Co-authored-by: Cursor <[email protected]>

* Fix iOS plist-derived path traversal

Select and validate one contained IPA app bundle, reject unsafe plist
executable values, and guard icon and Mach-O inputs before access.
Harden equivalent source icon, plist, library, and dynamic icon paths.

Co-authored-by: Cursor <[email protected]>

* correct version

* fix CSRF in dynamic analysis actions

Co-authored-by: Ajin Abraham <[email protected]>

* document HTTPTools host header SSRF advisory

Co-authored-by: Ajin Abraham <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Ajin Abraham
2026-09-20 16:36:10 -07:00
committed by GitHub
co-authored by Cursor
parent e2609ad5aa
commit a23ff75ec0
12 changed files with 164 additions and 79 deletions
+2
View File
@@ -10,6 +10,8 @@ Please report all security issues [here](https://github.com/MobSF/Mobile-Securit
| Vulnerability | Affected Versions |
| ------- | ------------------ |
| [Cross-Site Request Forgery via GET-based dynamic analysis actions](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-hh7q-v28p-p55m) | `<=4.5.3` |
| [SSRF via attacker-controlled Host header in HTTPTools shutdown](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-3fr8-jfgg-fm3x) | `<=4.5.3` |
| [Path traversal in iOS IPA icon extraction via unvalidated CFBundleExecutable](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-m83p-3cgp-6p8c) | `<=4.5.3` |
| [Denial of service via APK decompression bomb in the bundled apkinspector ZIP reader](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-cvhc-xjjc-c4p3) | `<=4.5.3` |
| [Windows-only path traversal via root-relative paths bypasses is_path_traversal()](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-c8g7-42qj-frj3) | `<=4.5.3` |
@@ -14,6 +14,7 @@ from django.http import (HttpResponseRedirect,
from django.conf import settings
from django.shortcuts import render
from django.db.models import ObjectDoesNotExist
from django.views.decorators.http import require_http_methods
from mobsf.DynamicAnalyzer.views.android.environment import (
ANDROID_API_SUPPORTED,
@@ -116,6 +117,7 @@ def android_dynamic_analysis(request, api=False):
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def dynamic_analyzer(request, checksum, api=False):
"""Android Dynamic Analyzer Environment."""
try:
@@ -123,12 +125,8 @@ def dynamic_analyzer(request, checksum, api=False):
activities = None
deeplinks = None
exported_activities = None
if api:
reinstall = request.POST.get('re_install', '1')
install = request.POST.get('install', '1')
else:
reinstall = request.GET.get('re_install', '1')
install = request.GET.get('install', '1')
reinstall = request.POST.get('re_install', '1')
install = request.POST.get('install', '1')
if not is_md5(checksum):
# We need this check since checksum is not validated
# in REST API
@@ -238,6 +236,7 @@ def dynamic_analyzer(request, checksum, api=False):
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def httptools_start(request):
"""Start httprools UI."""
logger.info('Starting httptools Web UI')
@@ -247,8 +246,8 @@ def httptools_start(request):
start_httptools_ui(settings.PROXY_PORT)
time.sleep(3)
logger.info('httptools UI started')
if request.GET['project']:
project = request.GET['project']
if request.POST['project']:
project = request.POST['project']
else:
project = ''
url = f'{httptools_url}/dashboard/{project}'
@@ -261,22 +260,23 @@ def httptools_start(request):
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['GET', 'POST'])
def logcat(request, api=False):
logger.info('Starting Logcat streaming')
try:
pkg = request.GET.get('package')
if pkg:
if not strict_package_check(pkg):
return print_n_send_error_response(
request,
'Invalid package name',
api)
template = 'dynamic_analysis/android/logcat.html'
return render(request, template, {'package': pkg})
if request.method == 'GET':
pkg = request.GET.get('package')
if pkg and strict_package_check(pkg):
template = 'dynamic_analysis/android/logcat.html'
return render(request, template, {'package': pkg})
return print_n_send_error_response(
request,
'Invalid package name',
api)
if api:
app_pkg = request.POST['package']
else:
app_pkg = request.GET.get('app_package')
app_pkg = request.POST.get('app_package')
if app_pkg:
if not strict_package_check(app_pkg):
return print_n_send_error_response(
@@ -306,6 +306,7 @@ def logcat(request, api=False):
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def trigger_static_analysis(request, checksum):
"""On device APK Static Analysis."""
try:
@@ -82,17 +82,13 @@ def dynamic_analysis_device(request, api=False):
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def dynamic_analyzer_device(request, api=False):
"""Dynamic Analyzer for Jailbroken iOS devices."""
try:
if api:
bundleid = request.POST.get('bundle_id')
device_id = request.POST.get('device_id')
form = None
else:
bundleid = request.GET.get('bundle_id')
device_id = request.GET.get('device_id')
form = UploadFileForm()
bundleid = request.POST.get('bundle_id')
device_id = request.POST.get('device_id')
form = None if api else UploadFileForm()
if not bundleid or not strict_package_check(bundleid):
return print_n_send_error_response(
request,
@@ -6,6 +6,7 @@ from pathlib import Path
from django.conf import settings
from django.shortcuts import render
from django.views.decorators.http import require_http_methods
from mobsf.MobSF.utils import (
common_check,
@@ -68,17 +69,13 @@ def dynamic_analysis(request, api=False):
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def dynamic_analyzer(request, api=False):
"""Dynamic Analyzer for in-device iOS apps."""
try:
if api:
bundleid = request.POST.get('bundle_id')
instance_id = request.POST.get('instance_id')
form = None
else:
bundleid = request.GET.get('bundle_id')
instance_id = request.GET.get('instance_id')
form = UploadFileForm()
bundleid = request.POST.get('bundle_id')
instance_id = request.POST.get('instance_id')
form = None if api else UploadFileForm()
if not bundleid or not strict_package_check(bundleid):
return print_n_send_error_response(
request,
@@ -161,12 +161,20 @@ textarea:focus {
{{ det.1}}
</td>
<td align="center">
<p>
<a class="btn btn-primary disable" onclick="dynamic_loader()" href="{% url "dynamic_analyzer" checksum=hash %}?re_install=0&install=0"><i class="fab fa-android"></i> Start Dynamic Analysis</a>
</p>
<p>
<a class="btn btn-warning disable" onclick="dynamic_loader()" href="{% url "static_scan" checksum=hash %}"><i class="fa fa-download"></i> Pull &amp; Static Analysis</a>
</p>
<div class="mb-3">
<form method="post" action="{% url "dynamic_analyzer" checksum=hash %}">
{% csrf_token %}
<input type="hidden" name="re_install" value="0">
<input type="hidden" name="install" value="0">
<button type="submit" class="btn btn-primary disable" onclick="dynamic_loader()"><i class="fab fa-android"></i> Start Dynamic Analysis</button>
</form>
</div>
<div class="mb-3">
<form method="post" action="{% url "static_scan" checksum=hash %}">
{% csrf_token %}
<button type="submit" class="btn btn-warning disable" onclick="dynamic_loader()"><i class="fa fa-download"></i> Pull &amp; Static Analysis</button>
</form>
</div>
<p>
<a class="btn btn-info" href="{% url "dynamic_report" checksum=hash %}"><i class="fa fa-mobile"></i> View Report </a>
</p>
@@ -202,12 +210,19 @@ textarea:focus {
{{ e.PACKAGE_NAME }}
</td>
<td align="center">
<p>
<a class="btn btn-success disable" onclick="dynamic_loader()" href="{% url "dynamic_analyzer" checksum=e.MD5 %}"><i class="fab fa-android"></i> Start Dynamic Analysis</a>
</p>
<p>
<a class="btn btn-primary btn-sm disable" onclick="dynamic_loader()" href="{% url "dynamic_analyzer" checksum=e.MD5 %}?re_install=0"><i class="fab fa-android"></i> Start Dynamic Analysis (No reinstall)</a>
</p>
<div class="mb-3">
<form method="post" action="{% url "dynamic_analyzer" checksum=e.MD5 %}">
{% csrf_token %}
<button type="submit" class="btn btn-success disable" onclick="dynamic_loader()"><i class="fab fa-android"></i> Start Dynamic Analysis</button>
</form>
</div>
<div class="mb-3">
<form method="post" action="{% url "dynamic_analyzer" checksum=e.MD5 %}">
{% csrf_token %}
<input type="hidden" name="re_install" value="0">
<button type="submit" class="btn btn-primary btn-sm disable" onclick="dynamic_loader()"><i class="fab fa-android"></i> Start Dynamic Analysis (No reinstall)</button>
</form>
</div>
<p>
<a class="btn btn-info {% if not e.DYNAMIC_REPORT_EXISTS %}disabled{% endif %}" href="{% url "dynamic_report" checksum=e.MD5 %}"><i class="fa fa-mobile"></i> View Report </a>
</p>
@@ -258,15 +258,19 @@
<div class="col-12">
<p><strong><i class="fas fa-info-circle"></i> INFORMATION</strong></p>
<div align="center">
<p>
<div class="mb-3">
{% if apimon %}
<a target="_blank" class="btn btn-primary" role="button" href="{% url 'live_api' %}?hash={{ hash }}&amp;package={{ package }}"><i class="fa fa-mobile"></i> API Monitor View</a>
{% endif %}
{% if frida_logs %}
<a target="_blank" class="btn btn-primary" role="button" href="{% url 'frida_logs' %}?hash={{ hash }}&amp;package={{ package }}"><i class="fas fa-spider"></i> Frida Logs View</a>
{% endif %}
<a class="btn btn-success" target="_blank" role="button" href="{% url 'httptools' %}?project={{package}}"><i class="fab fa-hire-a-helper"></i> Start HTTPTools</a>
</p>
<form method="post" action="{% url 'httptools' %}" target="_blank" class="d-inline">
{% csrf_token %}
<input type="hidden" name="project" value="{{ package }}">
<button type="submit" class="btn btn-success"><i class="fab fa-hire-a-helper"></i> Start HTTPTools</button>
</form>
</div>
<p><h6><strong>Raw Logs</strong></h6></p>
<p>
{% if droidmon %}
@@ -1,26 +1,72 @@
<div id="messages"></div>
<button id="tgl" onclick="toggle()">Stop Streaming</button>
<script type="text/javascript">
var source = new EventSource('/logcat/?app_package={{package}}');
var i = 0;
var stream = true;
var div = document.getElementById("messages");
source.onmessage = function(message) {
var controller = new AbortController();
function renderMessage(data) {
if (stream) {
// Grep package name
if (!message.data.includes('{{package}}'))
if (!data.includes('{{ package|escapejs }}'))
return;
i = i + 1;
// Display only last 500 lines
if (i < 500){
div.innerHTML = div.innerHTML + html_safe(message.data) + "<br>" ;
div.innerHTML = div.innerHTML + html_safe(data) + "<br>" ;
} else {
div.innerHTML = html_safe(message.data) + "<br>";
div.innerHTML = html_safe(data) + "<br>";
i=0;
}
window.scrollTo(0,document.body.scrollHeight);
}
};
}
async function startStream() {
const body = new URLSearchParams();
body.append('app_package', '{{ package|escapejs }}');
try {
const response = await fetch('/logcat/', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': '{{ csrf_token }}'
},
body: body,
signal: controller.signal
});
if (!response.ok || !response.body) {
div.textContent = 'Unable to start logcat stream.';
return;
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const result = await reader.read();
if (result.done)
break;
buffer += decoder.decode(result.value, {stream: true});
const messages = buffer.split('\n\n');
buffer = messages.pop();
messages.forEach(function(message) {
if (message.startsWith('data:'))
renderMessage(message.substring(5));
});
}
buffer += decoder.decode();
if (buffer.startsWith('data:'))
renderMessage(buffer.substring(5));
} catch (error) {
if (error.name !== 'AbortError')
div.textContent = 'Logcat stream ended unexpectedly.';
}
}
window.addEventListener('beforeunload', function() {
controller.abort();
});
startStream();
function toggle(){
var btn = document.getElementById("tgl");
@@ -259,17 +259,23 @@ function list_apps(){
else
order = 'last';
var url = `{% url 'dynamic_analyzer_device' %}?bundle_id=${escapeHtml(bundle)}&device_id=${$('#ios_dynamic').val()}`;
var url = `{% url 'dynamic_analyzer_device' %}`;
var deviceId = escapeHtml($('#ios_dynamic').val());
$('#in_device > tbody:' + order + '-child').append(
`<tr><td align="center">
<img id="app_icon" src="${escapeHtml(iconb64)}"/><br/>
<strong>${escapeHtml(name)}</strong></td>
<td>${escapeHtml(bundle)}</td>
<td>${escapeHtml(type)}</td>
<td><p>
<a class="btn btn-success disable" onclick="dynamic_loader()" href="${url}"><i class="fab fa-apple"></i> Start Dynamic Analysis</a>
<td><div>
<form method="post" action="${url}" onsubmit="dynamic_loader()" class="d-inline">
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
<input type="hidden" name="bundle_id" value="${escapeHtml(bundle)}">
<input type="hidden" name="device_id" value="${deviceId}">
<button type="submit" class="btn btn-success disable"><i class="fab fa-apple"></i> Start Dynamic Analysis</button>
</form>
<a class="btn btn-info ${buttonState}" onclick="dynamic_loader()" href="../../ios/view_report_device/${escapeHtml(bundle)}"><i class="fa fa-mobile"></i> View Report </a>
</p>
</div>
</td></tr>`);
}
$('.upload-install-btn').removeClass('disabled');
@@ -296,13 +296,17 @@ pre {
<div class="col-12">
<p><strong><i class="fas fa-info-circle"></i> INFORMATION</strong></p>
<div align="center">
<p>
<div class="mb-3">
{% if frida_logs %}
<a target="_blank" class="btn btn-primary" role="button" href="{% url 'ios_api_monitor' %}?checksum={{ hash }}&bundle_id={{ bundleid }}"><i class="fa fa-search"></i> API Monitor Logs</a>
<a target="_blank" class="btn btn-primary" role="button" href="{% url 'frida_logs' %}?hash={{ hash }}"><i class="fas fa-spider"></i> Frida Logs View</a>
{% endif %}
<a class="btn btn-success" target="_blank" role="button" href="{% url 'httptools' %}?project={{bundleid}}"><i class="fab fa-hire-a-helper"></i> Start HTTPTools</a>
</p>
<form method="post" action="{% url 'httptools' %}" target="_blank" class="d-inline">
{% csrf_token %}
<input type="hidden" name="project" value="{{ bundleid }}">
<button type="submit" class="btn btn-success"><i class="fab fa-hire-a-helper"></i> Start HTTPTools</button>
</form>
</div>
<p><h6><strong>Raw Logs</strong></h6></p>
<p>
<a target="_blank" class="btn btn-primary btn-sm" role="button" href="{% url 'download' %}{{ hash }}-web_traffic.txt"><i class="fas fa-water"></i> HTTP(S) Traffic</a>
@@ -393,18 +393,24 @@ function list_apps(){
else
order = 'last';
var url = `{% url 'dynamic_analyzer_ios'%}?bundle_id=${escapeHtml(bundle)}&instance_id=${$('#ios_dynamic').val()}`;
var url = `{% url 'dynamic_analyzer_ios'%}`;
var instanceId = escapeHtml($('#ios_dynamic').val());
$('#in_device > tbody:' + order + '-child').append(
`<tr><td align="center">
<img id="app_icon" src="data:image/png;base64,${escapeHtml(iconb64)}"/><br/>
<strong>${escapeHtml(name)}</strong></td>
<td>${escapeHtml(bundle)}</td>
<td>${escapeHtml(type)}</td>
<td><p>
<a class="btn btn-success disable" onclick="dynamic_loader()" href="${url}"><i class="fab fa-apple"></i> Start Dynamic Analysis</a>
<td><div>
<form method="post" action="${url}" onsubmit="dynamic_loader()" class="d-inline">
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
<input type="hidden" name="bundle_id" value="${escapeHtml(bundle)}">
<input type="hidden" name="instance_id" value="${instanceId}">
<button type="submit" class="btn btn-success disable"><i class="fab fa-apple"></i> Start Dynamic Analysis</button>
</form>
<a class="btn btn-info ${buttonState}" href="../../ios/view_report/${escapeHtml(bundle)}"><i class="fa fa-mobile"></i> View Report </a>
<a class="btn btn-warning" id="${$('#ios_dynamic').val()}" onclick="remove_app(this, '${escapeHtml(bundle)}')"><i class="fas fa-trash-alt"></i> Uninstall</a>
</p>
</div>
</td></tr>`);
}
stop_loader();
@@ -296,13 +296,17 @@ pre {
<div class="col-12">
<p><strong><i class="fas fa-info-circle"></i> INFORMATION</strong></p>
<div align="center">
<p>
<div class="mb-3">
{% if frida_logs %}
<a target="_blank" class="btn btn-primary" role="button" href="{% url 'ios_api_monitor' %}?checksum={{ hash }}&bundle_id={{ bundleid }}"><i class="fa fa-search"></i> API Monitor Logs</a>
<a target="_blank" class="btn btn-primary" role="button" href="{% url 'frida_logs' %}?hash={{ hash }}"><i class="fas fa-spider"></i> Frida Logs View</a>
{% endif %}
<a class="btn btn-success" target="_blank" role="button" href="{% url 'httptools' %}?project={{bundleid}}"><i class="fab fa-hire-a-helper"></i> Start HTTPTools</a>
</p>
<form method="post" action="{% url 'httptools' %}" target="_blank" class="d-inline">
{% csrf_token %}
<input type="hidden" name="project" value="{{ bundleid }}">
<button type="submit" class="btn btn-success"><i class="fab fa-hire-a-helper"></i> Start HTTPTools</button>
</form>
</div>
<p><h6><strong>Raw Logs</strong></h6></p>
<p>
<a target="_blank" class="btn btn-primary btn-sm" role="button" href="{% url 'download' %}{{ hash }}-web_traffic.txt"><i class="fas fa-water"></i> HTTP(S) Traffic</a>
@@ -368,12 +368,13 @@
{% else %}
{% if app_type not in 'jar,aar,so' %}
<li class="nav-item">
<a onclick="dynamic_loader()" href="{% url "dynamic_analyzer" checksum=md5 %}" class="nav-link">
<i class="nav-icon fa fa-play-circle"></i>
<p>
Start Dynamic Analysis
</p>
</a>
<form method="post" action="{% url "dynamic_analyzer" checksum=md5 %}">
{% csrf_token %}
<button type="submit" onclick="dynamic_loader()" class="nav-link btn btn-link text-left w-100">
<i class="nav-icon fa fa-play-circle"></i>
<p>Start Dynamic Analysis</p>
</button>
</form>
</li>
{% endif %}
{% endif %}
@@ -628,12 +629,15 @@
<a id="supbtn" onclick="list_suppressions()" role="button" class="btn btn-primary" data-target="#sup_list" data-toggle="modal" href="#"><i class="fa fa-table" aria-hidden="true"></i> Manage Suppressions</a>
{% endif %}
</p>
<p>
<div>
{% if app_type not in 'jar,aar,so' %}
<a onclick="dynamic_loader()" href="{% url 'dynamic_analyzer' checksum=md5 %}" class="btn btn-success" role="button"><i class="fa fa-play-circle"></i> Start Dynamic Analysis</a>
<form method="post" action="{% url 'dynamic_analyzer' checksum=md5 %}" class="d-inline">
{% csrf_token %}
<button type="submit" onclick="dynamic_loader()" class="btn btn-success"><i class="fa fa-play-circle"></i> Start Dynamic Analysis</button>
</form>
{% endif %}
<button type="button" class="btn btn-warning" data-toggle="modal" data-target=".scan-logs-mdl"><i class="fa fa-list" aria-hidden="true"></i> Scan Logs</button>
</p>
</div>
</div>
</div><!-- /.card -->