mirror of
https://github.com/MobSF/Mobile-Security-Framework-MobSF.git
synced 2026-10-11 22:50:09 +00:00
[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:
@@ -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` |
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -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]>"]
|
||||
|
||||
Reference in New Issue
Block a user