[HOTFIX][Security] Security Updates July 5 2026 (#2627)

Security Fixes

[GHSA-8j49-mmcx-4mp5] Arbitrary File Read via Path Traversal in ZIP/APK Icon Extraction
The android:icon attribute from an APK manifest was interpolated into file paths without validation, allowing a crafted APK to read arbitrary files from the server. Fixed by adding is_path_traversal() and is_safe_path() guards in find_icon_path_zip. Dependency bump included.

[GHSA-3p54-567p-2wpr] CSRF Checks Not Enforced After Django Middleware Migration
The migration from the deprecated MIDDLEWARE_CLASSES to MIDDLEWARE omitted CsrfViewMiddleware, SecurityMiddleware, and XFrameOptionsMiddleware, leaving CSRF, HSTS, and clickjacking protections silently disabled. All three have been restored to the active MIDDLEWARE tuple. The now-dead MIDDLEWARE_CLASSES block has been removed to prevent future confusion.

[GHSA-x768-8642-mmq9] Zip Bomb Denial of Service via Per-File Size Limit Bypass
The per-file size check in ZIP extraction logged a warning on oversized members but was missing a continue, allowing files exceeding ZIP_MAX_UNCOMPRESSED_FILE_SIZE to be extracted anyway as long as the aggregate limit was not reached. Fixed by adding the missing continue.

[GHSA-95px-34x5-p37h] SSRF Port Restriction Bypass in assetlinks_check
valid_host() was called with only the bare hostname, allowing android:port to be appended afterward without going through port validation, bypassing the HTTP/HTTPS-only restriction. Fixed with a two-layer approach: primary port allowlist check before URL assembly in get_browsable_activities, and a defence-in-depth re-check in _check_url.

Hardening

The hand-rolled safe_extract used os.path.abspath (symlink-blind) instead of os.path.realpath, creating a TOCTOU window where a symlink could redirect extraction outside the destination directory. Replaced with Python 3.12's tarfile.extractall(filter='data') (PEP 706), which rejects symlinks, hardlinks, absolute paths, and traversal members per-member before extraction. A robust realpath-based fallback is included for older Python versions.
Both download_app_data call sites in the iOS dynamic analyser now wrap app_container and tarfile with shlex.quote when building the SSH tar command, as defensive coding hygiene.
Refactoring

is_path_traversal, is_safe_path, clean_filename, cmd_injection_check, is_pipe_or_link, and is_attack_pattern were scattered across utils.py and shared.py. All have been moved to mobsf/MobSF/security.py as the single authoritative location for security primitives.
Developer Tooling

AGENTS.md / CLAUDE.md - AI agent guidelines
Added a coding-standards document for AI coding agents (Cursor, Claude, Codex) covering MobSF-specific secure-by-default patterns: path traversal guards, archive extraction safety, Django middleware active-tuple hygiene, split-validation anti-patterns (SSRF port bypass), guard completeness (continue/return/raise after every security check), and Django-specific security features (form validators, decorators, middleware). CLAUDE.md is a symlink to AGENTS.md.
This commit is contained in:
Ajin Abraham
2026-07-05 16:55:49 -07:00
committed by GitHub
parent c098134845
commit 62563ca429
30 changed files with 945 additions and 697 deletions
+4
View File
@@ -10,6 +10,10 @@ Please report all security issues [here](https://github.com/MobSF/Mobile-Securit
| Vulnerability | Affected Versions |
| ------- | ------------------ |
| [SSRF port restriction bypass in assetlinks_check](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-95px-34x5-p37h) | `<=4.5.0` |
| [Zip Bomb Denial of Service via Per-File Size Limit Bypass in ZIP/APK](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-x768-8642-mmq9) | `<=4.5.0` |
| [Regression: CSRF checks not enforced after Django migration](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-3p54-567p-2wpr) | `<=4.5.0` |
| [Arbitrary File Read via Path Traversal in ZIP Uploads](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-8j49-mmcx-4mp5) | `<=4.5.0` |
| [SQL Injection in SQLite Database Viewer utils](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-hqjr-43r5-9q58) | `<=4.4.5` |
| [Stored XSS via Manifest Analysis - Dialer Code Host Field](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-8hf7-h89p-3pqj) | `<=4.4.4` |
| [Arbitrary File Write (AR-Slip) via Absolute Path in .a Extraction](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-9gh8-9r95-3fc3) | `<=4.4.0` |
+234
View File
@@ -0,0 +1,234 @@
# MobSF Agent Guidelines
MobSF is a security analysis platform. Every code path processes attacker-supplied
input (APKs, ZIPs, IPAs, manifests) from authenticated but potentially malicious
users. Security must be the default, not an afterthought.
---
## Code Quality — Mandatory Before Every Commit
Run lint and fix all errors before finishing any task:
```bash
tox -e lint
```
Never leave a task with a non-zero exit code from this command.
---
## Security Architecture
Centralized security helpers live in **`mobsf/MobSF/security.py`**. When adding new
security checks, prefer adding them there. Some legacy validators still live in
`mobsf/MobSF/utils.py`; use existing helpers where they are already established.
### Available Security Functions
Import only the helpers needed for the change:
```python
from mobsf.MobSF.security import (
# Path safety
is_path_traversal, # Check raw string for .. sequences, absolute paths, URL encoding tricks
is_safe_path, # Containment check after path construction via realpath()
# Input validation
is_attack_pattern, # Detect shell injection: ;, $(), ||, &&
cmd_injection_check, # Detect OS command injection characters
is_pipe_or_link, # Detect symlinks and named FIFOs before reading files
# Output sanitization
sanitize_filename, # Safe filename for Content-Disposition headers
sanitize_for_logging,# Strip newlines and control chars before logging user input
sanitize_redirect, # Allow only relative paths in redirects
sanitize_svg, # Strip XSS vectors from SVG content (bleach-based)
clean_filename, # Windows-safe filename (unicode normalization)
# Network / SSRF
valid_host, # DNS-resolves host; rejects private/loopback/multicast IPs
)
```
---
## Past Vulnerabilities and Insecure Patterns
Read `.github/SECURITY.md` to understand the full history of security issues in this
codebase. Use it as a guide for what classes of bugs to watch for and what patterns
have been exploited before. When in doubt about whether a pattern is safe, check
whether a similar pattern has appeared in the advisory history.
---
## Incomplete Fix Anti-Pattern
The most common source of security regressions in this codebase is applying a fix to
one code path but not its siblings. Before closing any security fix:
1. Search for all functions or patterns that perform the same operation (e.g., every
place that resolves an icon path, every place that extracts an archive entry).
2. Verify the fix is applied consistently across **all** of them.
3. Check both the APK binary flow and the source-ZIP flow — they are separate code paths
with separate callsites and have diverged in the past.
---
## Input Trust Model
- `request.GET` / `request.POST`: untrusted. Validate with forms or explicit checks;
escape on output.
- File uploads: untrusted. Validate magic bytes, size limits, and extension allowlists.
- Archive entries (`zip`, `tar`, `ar`): untrusted. Check each entry before extraction.
- `AndroidManifest.xml` values: untrusted. Treat as attacker-controlled before using
them in filesystem operations or rendering them.
- `Info.plist` values: untrusted. Apply the same treatment as manifest values.
- `md5` / `hash` URL parameters: semi-trusted only after validation. Always validate
with `is_md5()` before using them in paths.
- Device identifiers: untrusted. Use command-injection checks plus format validation.
---
## Django-Specific Security Features
### Form Validation — The Primary Input Sanitization Layer
Prefer Django forms for new request validation. If a view does not use a form, validate
every `request.GET[...]` or `request.POST[...]` value explicitly before using it.
The project uses a mixin composition pattern. Combine the appropriate mixins rather than
writing ad-hoc validation in view code:
```python
# StaticAnalyzer/forms.py — mixins to compose from
AttackDetect # is_path_traversal + extension allowlist on a 'file' param
APIChecks # MD5 format check on a 'hash' param (API mode)
WebChecks # MD5 format check on an 'md5' param (HTML mode)
AndroidChecks # ChoiceField allowlist for Android scan type
IOSChecks # ChoiceField allowlist for iOS scan type
```
Custom field validators belong in a `clean_<field>()` method that raises
`forms.ValidationError` on rejection — never return a partial result and check it in
the view. `FormUtil.errors_message(form)` produces the standard error envelope to return
to the caller when `form.is_valid()` is False.
**Use `ChoiceField` for any parameter with a finite set of valid values.** This
eliminates an entire class of injection risk at the form layer with no extra code.
Never use `CharField` and then manually compare the value against an allowlist in the
view — let the form do it.
### View Decorators — Apply All Three
Views that handle sensitive operations should use the applicable Django decorators for
authentication, authorization, and method restriction:
```python
@login_required
@permission_required(Permissions.SCAN) # or DELETE, SUPPRESS, etc.
@require_http_methods(['POST']) # or ['GET'] — never omit this
def my_view(request, api=False):
...
```
- `@login_required` blocks unauthenticated access.
- `@permission_required` enforces role-based access beyond authentication.
- `@require_http_methods` rejects wrong HTTP verbs before any logic runs,
preventing CSRF-via-GET and other method-confusion issues.
### Template Auto-Escaping
Django's template engine escapes variables by default. Do **not** use `{% autoescape off %}`
or the `|safe` filter on any value derived from scan data, manifests, or user input.
When rendering user-controlled strings outside of templates (e.g., in a JSON response
built by hand), use `django.utils.html.escape()` explicitly.
### ORM — No Raw SQL
Use the Django ORM for all database access. Never use `.raw()` or string-formatted SQL.
When a queryset filter value comes from user input, pass it as a keyword argument
(the ORM parameterizes it automatically):
```python
# Correct
RecentScansDB.objects.filter(MD5=checksum)
# Wrong
RecentScansDB.objects.raw(f'SELECT * FROM ... WHERE MD5 = "{checksum}"')
```
### CSRF
Django's `CsrfViewMiddleware` is enabled globally. Do not use `@csrf_exempt` on any
view that modifies state. API endpoints that accept an `X-Csrftoken` header or use
token-based auth are the only legitimate exception, and that pattern is already
established in the existing API views.
---
## Archive Extraction Safety
### TAR
Never use a hand-rolled name-only check with `os.path.abspath`. The symlink +
nested-entry combination bypasses it: a symlink member named `escape` passes the
name check, gets extracted to disk, and then a file member named `escape/pwned.txt`
is written through the symlink to an arbitrary location.
`os.path.abspath` normalises `..` but does **not** resolve symlinks.
`os.path.realpath` resolves both — but even `realpath`-based checks that run before
extraction have a TOCTOU window.
Use Python 3.12's built-in filter instead (MobSF requires `python = "^3.12"`):
```python
# Correct — per-member, type-aware, symlink-aware
tar.extractall(dest, members=safe_members_generator, filter='data')
# Wrong — abspath-based name check; blind to symlinks
for member in tar.getmembers():
if not os.path.abspath(join(dest, member.name)).startswith(dest):
raise ...
tar.extractall(dest, members=...)
```
`filter='data'` rejects: symlinks outside destination, hardlinks outside destination,
absolute paths, path traversal, and device files — per member, before extraction.
For code that must support Python < 3.12, fall back to: skip all symlink and hardlink
members (`member.issym()` / `member.islnk()`), then use `realpath` for the boundary
check, and validate-then-extract per member rather than batch-validate-then-extractall.
### ZIP
Python's `zipfile` module does not create real filesystem symlinks from Unix symlink
entries — it writes the link target as plain file bytes. The TAR symlink attack does
not apply to ZIP extraction. Use `is_path_traversal` + `is_safe_path` for member name
validation and validate per-member before calling `zip_ref.extract(member, dest)`.
---
## Import Conventions
When adding new imports, maintain alphabetical order within each import group to satisfy
`flake8-import-order`. Group order: stdlib → third-party → Django → local MobSF.
---
## Checklist for Any Change That Touches File I/O or User Input
- [ ] Raw input validated with `is_path_traversal` before path construction
- [ ] Constructed filesystem paths verified with `is_safe_path` when a safe root exists
- [ ] Symlinks and FIFOs rejected with `is_pipe_or_link` before file reads
- [ ] Shell arguments passed as a list, not a formatted string
- [ ] User-controlled strings escaped with `django.utils.html.escape` before rendering
- [ ] SVG content piped through `sanitize_svg`
- [ ] Outbound URLs checked with `valid_host`
- [ ] Redirects wrapped in `sanitize_redirect`
- [ ] Log statements use `sanitize_for_logging` on any user-derived value
- [ ] TAR extraction uses `filter='data'` — not a hand-rolled `abspath` check
- [ ] ZIP extraction validates each member path with `realpath` before `extract()`
- [ ] Every security guard has `continue` / `return` / `raise` — logging alone is not a guard
- [ ] Fix applied symmetrically to all equivalent code paths
- [ ] `tox -e lint` passes with exit code 0
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
@@ -14,14 +14,14 @@ from django.views.decorators.http import require_http_methods
from mobsf.DynamicAnalyzer.views.common.shared import (
invalid_params,
is_attack_pattern,
send_response,
)
from mobsf.MobSF.security import is_attack_pattern
from mobsf.DynamicAnalyzer.views.android.environment import (
Environment,
)
from mobsf.MobSF.security import cmd_injection_check
from mobsf.MobSF.utils import (
cmd_injection_check,
docker_translate_localhost,
get_adb,
get_device,
@@ -27,8 +27,8 @@ from mobsf.DynamicAnalyzer.tools.webproxy import (
get_http_tools_url,
stop_httptools,
)
from mobsf.MobSF.security import cmd_injection_check
from mobsf.MobSF.utils import (
cmd_injection_check,
is_md5,
python_list,
)
@@ -15,9 +15,9 @@ from django.views.decorators.http import require_http_methods
from mobsf.DynamicAnalyzer.views.android.frida_core import Frida
from mobsf.DynamicAnalyzer.views.common.shared import (
invalid_params,
is_attack_pattern,
send_response,
)
from mobsf.MobSF.security import is_attack_pattern
from mobsf.DynamicAnalyzer.views.android.operations import (
get_package_name,
)
+1 -1
View File
@@ -11,9 +11,9 @@ from django.utils.html import escape
from mobsf.MobSF.views.authentication import (
login_required,
)
from mobsf.MobSF.security import is_safe_path
from mobsf.MobSF.utils import (
is_md5,
is_safe_path,
print_n_send_error_response,
read_sqlite,
)
@@ -9,9 +9,9 @@ from django.views.decorators.http import require_http_methods
from mobsf.DynamicAnalyzer.views.common.shared import (
send_response,
)
from mobsf.MobSF.security import is_safe_path
from mobsf.MobSF.utils import (
is_md5,
is_safe_path,
print_n_send_error_response,
)
from mobsf.MobSF.views.authentication import (
+29 -29
View File
@@ -15,11 +15,10 @@ from mobsf.MalwareAnalyzer.views.MalwareDomainCheck import (
MalwareDomainCheck,
)
from mobsf.MobSF.exceptions import PathTraversalError
from mobsf.MobSF.security import clean_filename, is_pipe_or_link
from mobsf.MobSF.utils import (
EMAIL_REGEX,
URL_REGEX,
clean_filename,
is_pipe_or_link,
)
logger = logging.getLogger(__name__)
@@ -87,24 +86,34 @@ def untar_files(tar_loc, untar_dir):
else:
os.makedirs(untar_dir)
with tarfile.open(tar_loc.as_posix(), errorlevel=1) as tar:
def is_within_directory(directory, target):
abs_directory = os.path.abspath(directory)
abs_target = os.path.abspath(target)
return (abs_target.startswith(abs_directory + os.sep)
or abs_target == abs_directory)
def safe_extract(tar, path='.',
members=None,
*,
numeric_owner=False):
for member in tar.getmembers():
member_path = os.path.join(path, member.name)
if not is_within_directory(path, member_path):
raise PathTraversalError('Attempted Path Traversal in Tar File')
tar.extractall(path, members, numeric_owner=numeric_owner)
safe_extract(tar, untar_dir, members=safe_paths(tar))
if hasattr(tarfile, 'data_filter'):
# Python 3.12+ (PEP 706) / backported to 3.11.4, 3.10.12, 3.9.17.
# Rejects symlinks outside destination, hardlinks, device files,
# and absolute/traversal paths per-member before extraction.
tar.extractall(
untar_dir,
members=safe_paths(tar),
filter='data')
else:
# Fallback for Python < 3.9.17 without the PEP 706 backport.
# Manually reject symlinks and hardlinks, then use realpath
# (not abspath) to verify the resolved path stays inside the
# destination before extracting each member.
safe_root = os.path.realpath(untar_dir)
safe_members = []
for member in safe_paths(tar):
if member.issym() or member.islnk():
logger.warning(
'Skipping link member in tar: %s', member.name)
continue
member_path = os.path.realpath(
os.path.join(safe_root, member.name))
if not (member_path.startswith(safe_root + os.sep)
or member_path == safe_root):
raise PathTraversalError(
'Attempted Path Traversal in Tar File')
safe_members.append(member)
tar.extractall(untar_dir, members=iter(safe_members))
except (FileExistsError, tarfile.ReadError):
logger.warning('Failed to extract tar file')
except Exception:
@@ -173,12 +182,3 @@ def invalid_params(api=False):
if api:
return data
return send_response(data)
def is_attack_pattern(user_input):
"""Check for attacks."""
atk_pattern = re.compile(r';|\$\(|\|\||&&')
stat = re.findall(atk_pattern, user_input)
if stat:
logger.error('Possible RCE attack detected')
return stat
@@ -2,6 +2,7 @@
"""Instance Operation APIs."""
import logging
import re
import shlex
import shutil
import time
from base64 import b64encode
@@ -737,7 +738,8 @@ def download_app_data(ci, checksum):
tarfile = f'/tmp/{checksum}-app-container.tar'
localtar = app_dir / f'{checksum}-app-container.tar'
ssh_execute_cmd(
target, f'tar -C {app_container} -cvf {tarfile} .')
target,
f'tar -C {shlex.quote(app_container)} -cvf {shlex.quote(tarfile)} .')
with target.open_sftp() as sftp:
sftp.get(tarfile, localtar)
target.close()
@@ -22,9 +22,9 @@ from mobsf.MobSF.utils import (
)
from mobsf.DynamicAnalyzer.views.common.shared import (
invalid_params,
is_attack_pattern,
send_response,
)
from mobsf.MobSF.security import is_attack_pattern
from mobsf.DynamicAnalyzer.forms import UploadFileForm
from mobsf.DynamicAnalyzer.views.ios.helpers import (
configure_proxy,
@@ -1,6 +1,7 @@
# -*- coding: utf_8 -*-
"""Dynamic Analyzer Reporting for iOS devices."""
import logging
import shlex
import shutil
from pathlib import Path
import json
@@ -66,7 +67,8 @@ def download_app_data_device(ios_device, checksum):
'utf-8').splitlines()[0].strip()
tarfile = f'/tmp/{checksum}-app-container.tar'
localtar = app_dir / f'{checksum}-app-container.tar'
ios_device.execute_command(f'tar -C {app_container} -cvf {tarfile} .')
ios_device.execute_command(
f'tar -C {shlex.quote(app_container)} -cvf {shlex.quote(tarfile)} .')
ios_device.download_file(tarfile, localtar)
if localtar.exists():
dst = Path(settings.DWD_DIR) / f'{checksum}-app_data.tar'
@@ -10,9 +10,9 @@ from mobsf.DynamicAnalyzer.views.ios.frida_core import (
)
from mobsf.DynamicAnalyzer.views.common.shared import (
invalid_params,
is_attack_pattern,
send_response,
)
from mobsf.MobSF.security import is_attack_pattern
from mobsf.DynamicAnalyzer.views.ios.corellium_apis import (
CorelliumInstanceAPI,
OK,
+1 -1
View File
@@ -18,7 +18,7 @@ from mobsf.install.windows.setup import windows_config_local
logger = logging.getLogger(__name__)
VERSION = '4.5.0'
VERSION = '4.5.1'
BANNER = r"""
__ __ _ ____ _____ _ _ ____
| \/ | ___ | |__/ ___|| ___|_ _| || | | ___|
+87 -1
View File
@@ -9,7 +9,11 @@ import sys
from shutil import which
from pathlib import Path
from platform import system
from urllib.parse import urlparse
import os
import stat
import string
import unicodedata
from urllib.parse import unquote, urlparse
from concurrent.futures import ThreadPoolExecutor
from mobsf.MobSF.utils import (
@@ -367,3 +371,85 @@ def sanitize_svg(svg_content):
attributes=safe_attrs,
strip=True,
)
def is_path_traversal(user_input):
"""Check for path traversal."""
if not user_input:
return False
# Disallow absolute paths and windows paths and backslashes
if os.path.isabs(user_input) or user_input.startswith(('\\', '//')):
logger.error('Path traversal attack detected with absolute path')
return True
# Normalize and decode URL-encoded characters
try:
# Handle URL decoding (e.g., %2e -> .)
decoded = unquote(user_input)
# Handle double URL decoding (e.g., %252e -> %2e -> .)
double_decoded = unquote(decoded)
except Exception:
logger.error('Path traversal attack detected with invalid URL encoding')
return True
# Check for path traversal in both original and decoded versions
dangerous_patterns = ['..', '../', '..\\', '..\\\\']
if any(pattern in user_input for pattern in dangerous_patterns):
logger.error('Path traversal attack detected with invalid path')
return True
if any(pattern in decoded for pattern in dangerous_patterns):
logger.error('Path traversal attack detected with invalid path')
return True
if any(pattern in double_decoded for pattern in dangerous_patterns):
logger.error('Path traversal attack detected with invalid path')
return True
return False
def is_safe_path(safe_root, check_path, raw_file):
"""Detect Path Traversal."""
if is_path_traversal(raw_file):
return False
safe_root = os.path.realpath(os.path.normpath(safe_root))
check_path = os.path.realpath(os.path.normpath(check_path))
return check_path.startswith(safe_root + os.sep) or check_path == safe_root
def clean_filename(filename, replace=' '):
"""Sanitize filename for Windows compatibility."""
if system() == 'Windows':
whitelist = f'-_.() {string.ascii_letters}{string.digits}'
for r in replace:
filename = filename.replace(r, '_')
cleaned_filename = unicodedata.normalize(
'NFKD', filename).encode('ASCII', 'ignore').decode()
return ''.join(c for c in cleaned_filename if c in whitelist)
return filename
def cmd_injection_check(data):
"""OS Cmd Injection check from Commix."""
breakers = [
';', '%3B', '&', '%26', '&&',
'%26%26', '|', '%7C', '||',
'%7C%7C', '%0a', '%0d%0a',
]
return any(i in data for i in breakers)
def is_pipe_or_link(path):
"""Check for named pipe or symlink."""
return os.path.islink(path) or stat.S_ISFIFO(os.stat(path).st_mode)
def is_attack_pattern(user_input):
"""Check for shell injection attack patterns."""
atk_pattern = re.compile(r';|\$\(|\|\||&&')
result = re.findall(atk_pattern, user_input)
if result:
logger.error('Possible RCE attack detected')
return result
+3 -12
View File
@@ -192,23 +192,14 @@ INSTALLED_APPS = (
'mobsf.MobSF',
'mobsf.MalwareAnalyzer',
)
MIDDLEWARE_CLASSES = (
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django_ratelimit.middleware.RatelimitMiddleware',
)
MIDDLEWARE = (
'django.middleware.security.SecurityMiddleware',
'mobsf.MobSF.views.api.api_middleware.RestApiAuthMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
ROOT_URLCONF = 'mobsf.MobSF.urls'
WSGI_APPLICATION = 'mobsf.MobSF.wsgi.application'
-78
View File
@@ -19,7 +19,6 @@ import stat
import sqlite3
import unicodedata
import threading
from urllib.parse import unquote
from pathlib import Path
from concurrent.futures import (
ThreadPoolExecutor,
@@ -572,11 +571,6 @@ def read_sqlite(sqlite_file):
return table_dict
def is_pipe_or_link(path):
"""Check for named pipe."""
return os.path.islink(path) or stat.S_ISFIFO(os.stat(path).st_mode)
def get_network():
"""Get Network IPs."""
ips = []
@@ -611,15 +605,6 @@ def get_proxy_ip(identifier):
return proxy_ip
def is_safe_path(safe_root, check_path, raw_file):
"""Detect Path Traversal."""
if is_path_traversal(raw_file):
return False
safe_root = os.path.realpath(os.path.normpath(safe_root))
check_path = os.path.realpath(os.path.normpath(check_path))
return check_path.startswith(safe_root + os.sep) or check_path == safe_root
def file_size(app_path):
"""Return the size of the file."""
return round(float(os.path.getsize(app_path)) / (1024 * 1024), 2)
@@ -645,30 +630,6 @@ def get_config_loc():
return 'MobSF/settings.py'
def clean_filename(filename, replace=' '):
if platform.system() == 'Windows':
whitelist = f'-_.() {string.ascii_letters}{string.digits}'
# replace spaces
for r in replace:
filename = filename.replace(r, '_')
# keep only valid ascii chars
cleaned_filename = unicodedata.normalize(
'NFKD', filename).encode('ASCII', 'ignore').decode()
# keep only whitelisted chars
return ''.join(c for c in cleaned_filename if c in whitelist)
return filename
def cmd_injection_check(data):
"""OS Cmd Injection from Commix."""
breakers = [
';', '%3B', '&', '%26', '&&',
'%26%26', '|', '%7C', '||',
'%7C%7C', '%0a', '%0d%0a',
]
return any(i in data for i in breakers)
def strict_package_check(user_input):
"""Strict package name check.
@@ -713,45 +674,6 @@ def common_check(instance_id):
return None
def is_path_traversal(user_input):
"""Check for path traversal."""
if not user_input:
return False
# Disallow absolute paths and windows paths and backslashes
if os.path.isabs(user_input) or user_input.startswith(('\\', '//')):
logger.error('Path traversal attack detected with absolute path')
return True
# Normalize and decode URL-encoded characters
try:
# Handle URL decoding (e.g., %2e -> .)
decoded = unquote(user_input)
# Handle double URL decoding (e.g., %252e -> %2e -> .)
double_decoded = unquote(decoded)
except Exception:
logger.error('Path traversal attack detected with invalid URL encoding')
return True
# Check for path traversal in both original and decoded versions
dangerous_patterns = ['..', '../', '..\\', '..\\\\']
# Check original filename
if any(pattern in user_input for pattern in dangerous_patterns):
logger.error('Path traversal attack detected with invalid path')
return True
# Check decoded versions
if any(pattern in decoded for pattern in dangerous_patterns):
logger.error('Path traversal attack detected with invalid path')
return True
if any(pattern in double_decoded for pattern in dangerous_patterns):
logger.error('Path traversal attack detected with invalid path')
return True
return False
def is_zip_magic(file_obj):
magic = file_obj.read(4)
file_obj.seek(0, 0)
+1 -1
View File
@@ -15,9 +15,9 @@ from mobsf.MobSF.views.scanning import (
add_to_recent_scan,
handle_uploaded_file,
)
from mobsf.MobSF.security import is_path_traversal
from mobsf.MobSF.utils import (
is_internet_available,
is_path_traversal,
is_zip_magic,
strict_package_check,
upstream_proxy,
+1 -2
View File
@@ -29,13 +29,12 @@ from mobsf.MobSF.utils import (
is_dir_exists,
is_file_exists,
is_md5,
is_safe_path,
key,
print_n_send_error_response,
python_dict,
)
from mobsf.MobSF.init import api_key
from mobsf.MobSF.security import sanitize_filename, sanitize_svg
from mobsf.MobSF.security import is_safe_path, sanitize_filename, sanitize_svg
from mobsf.MobSF.views.helpers import FileType
from mobsf.MobSF.views.scanning import Scanning
from mobsf.MobSF.views.apk_downloader import apk_download
+1 -1
View File
@@ -3,9 +3,9 @@ from pathlib import Path
from django import forms
from mobsf.MobSF.security import is_path_traversal
from mobsf.MobSF.utils import (
is_md5,
is_path_traversal,
)
@@ -14,11 +14,14 @@ from lxml import etree
from django.conf import settings
from mobsf.MobSF.security import (
is_path_traversal,
is_safe_path,
)
from mobsf.MobSF.utils import (
append_scan_status,
find_java_binary,
is_file_exists,
is_path_traversal,
)
from mobsf.StaticAnalyzer.tools.androguard4 import (
axml,
@@ -115,21 +118,33 @@ def find_icon_path_zip(checksum, res_dir, icon_paths_from_manifest):
logger.info(msg)
append_scan_status(checksum, msg)
for icon_path in icon_paths_from_manifest:
if is_path_traversal(icon_path):
logger.warning('Path traversal detected in icon path: %s', icon_path)
continue
if icon_path.startswith('@'):
path_array = icon_path.strip('@').split(os.sep)
rel_path = os.sep.join(path_array[1:])
for size_str in KNOWN_MIPMAP_SIZES:
tmp_path = os.path.join(
res_dir, path_array[0] + size_str, rel_path + '.png')
if not is_safe_path(res_dir, tmp_path, icon_path):
continue
if os.path.exists(tmp_path):
return tmp_path
elif icon_path.startswith(('res/', '/res/')):
stripped_relative_path = icon_path.strip(
'/res') # Works for neither /res and res
full_path = os.path.join(res_dir, stripped_relative_path)
# Use removeprefix-style stripping to remove only the res/ prefix,
# not a character set (str.strip('/res') strips chars {/,r,e,s}).
rel = icon_path.lstrip('/')
if rel.startswith('res/'):
rel = rel[len('res/'):]
full_path = os.path.join(res_dir, rel)
if not is_safe_path(res_dir, full_path, icon_path):
continue
if os.path.exists(full_path):
return full_path
full_path += '.png'
if not is_safe_path(res_dir, full_path, icon_path):
continue
if os.path.exists(full_path):
return full_path
@@ -107,14 +107,17 @@ def _check_url(host, w_url):
urls.add(f'https://{w_url[7:]}')
for url in urls:
# Additional checks to ensure that
# the final path is WELL_KNOWN_PATH
# Validate the fully-assembled URL — path, port, query, and params.
purl = urlparse(url)
if (purl.path != WELL_KNOWN_PATH
or len(purl.query) > 0
or len(purl.params) > 0):
logger.warning('Invalid Assetlinks URL: %s', url)
continue
if purl.port and purl.port not in (80, 443):
logger.warning(
'Non-standard port in assetlinks URL rejected: %s', url)
continue
r = requests.get(url,
timeout=5,
allow_redirects=False,
@@ -190,6 +193,12 @@ def get_browsable_activities(node, ns):
continue
shost = f'{scheme}://{host}'
if port and is_number(port):
if int(port) not in (80, 443):
logger.warning(
'Non-standard port rejected in assetlinks '
'check (port %s bypasses valid_host): %s',
port, host)
continue
c_url = f'{shost}:{port}{WELL_KNOWN_PATH}'
else:
c_url = f'{shost}{WELL_KNOWN_PATH}'
@@ -5,9 +5,9 @@ from pathlib import Path
from defusedxml.minidom import parseString
from mobsf.MobSF.security import is_path_traversal
from mobsf.MobSF.utils import (
append_scan_status,
is_path_traversal,
)
logger = logging.getLogger(__name__)
@@ -10,8 +10,8 @@ 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.utils import (
is_safe_path,
print_n_send_error_response,
)
from mobsf.StaticAnalyzer.views.common.shared_func import (
+1 -1
View File
@@ -9,11 +9,11 @@ from pathlib import Path
from django.conf import settings
from mobsf.StaticAnalyzer.views.common.shared_func import unzip
from mobsf.MobSF.security import is_safe_path
from mobsf.MobSF.utils import (
append_scan_status,
find_java_binary,
is_file_exists,
is_safe_path,
)
logger = logging.getLogger(__name__)
@@ -23,14 +23,16 @@ from mobsf.MobSF import settings
from mobsf.MobSF.security import (
sanitize_for_logging,
)
from mobsf.MobSF.security import (
is_path_traversal,
is_safe_path,
)
from mobsf.MobSF.utils import (
EMAIL_REGEX,
STRINGS_REGEX,
URL_REGEX,
append_scan_status,
is_md5,
is_path_traversal,
is_safe_path,
print_n_send_error_response,
set_permissions,
)
@@ -158,6 +160,7 @@ def unzip(checksum, app_path, ext_path):
msg = (f'File too large ({size_mb:.2f} MB). Skipping '
f'{sanitize_for_logging(file_path)}')
logger.warning(msg)
continue
if total_size > settings.ZIP_MAX_UNCOMPRESSED_TOTAL_SIZE:
stop_fallback_extraction = True
total_size_mb = total_size / (1024 * 1024)
@@ -15,9 +15,9 @@ from mobsf.StaticAnalyzer.models import (
)
from mobsf.DynamicAnalyzer.views.common.shared import (
invalid_params,
is_attack_pattern,
send_response,
)
from mobsf.MobSF.security import is_attack_pattern
from mobsf.MobSF.utils import (
android_component,
is_md5,
@@ -14,9 +14,9 @@ from django.shortcuts import render
from django.utils.html import escape
from mobsf.MobSF.forms import FormUtil
from mobsf.MobSF.security import is_safe_path
from mobsf.MobSF.utils import (
is_file_exists,
is_safe_path,
print_n_send_error_response,
read_sqlite,
)
Generated
+528 -548
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "mobsf"
version = "4.5.0"
version = "4.5.1"
description = "Mobile Security Framework (MobSF) is an automated, all-in-one mobile application (Android/iOS/Windows) pen-testing, malware analysis and security assessment framework capable of performing static and dynamic analysis."
keywords = ["mobsf", "mobile security framework", "mobile security", "security tool", "static analysis", "dynamic analysis", "malware analysis"]
authors = ["Ajin Abraham <[email protected]>"]