mirror of
https://github.com/MobSF/Mobile-Security-Framework-MobSF.git
synced 2026-10-11 22:50:09 +00:00
[SECURITY] Improve SSRF checks, strict path check for well_known_path (#2510)
* Improved SSRF checks (credential checks, length check, port check, path, query, and params check, ipv6, ipv4 coverage, handle possible decimal or hex IP bypasses) * Add additional strict path check for Applink well known path * Moved `valid_host` to `security.py` * Update `security.md` * Bump dependencies * Fix docker build
This commit is contained in:
+2
-1
@@ -4,12 +4,13 @@ Keeping MobSF updated to the latest version is essential for ensuring security a
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report all security issues [here](https://github.com/MobSF/Mobile-Security-Framework-MobSF/issues) or email ajin25(gmail). We believe in coordinated and responsible disclosure.
|
||||
Please report all security issues [here](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/new) or email ajin25(gmail). We believe in coordinated and responsible disclosure.
|
||||
|
||||
## Past Security Issues
|
||||
|
||||
| Vulnerability | Affected Versions |
|
||||
| ------- | ------------------ |
|
||||
| [SSRF on assetlinks_check with DNS Rebinding](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-fcfq-m8p6-gw56) | `<=4.3.1` |
|
||||
| [Partial Denial of Service due to strict regex check in iOS report view URL](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-jrm8-xgf3-fwqr) | `<=4.3.0` |
|
||||
| [Local Privilege escalation due to leaked REST API key in web UI](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-79f6-p65j-3m2m) | `<=4.3.0` |
|
||||
| [Stored Cross-Site Scripting in iOS dynamic_analysis view via `bundle` id](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-cxqq-w3x5-7ph3) | `<=4.3.0` |
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ COPY pyproject.toml .
|
||||
RUN poetry config virtualenvs.create false && \
|
||||
poetry lock && \
|
||||
poetry install --only main --no-root --no-interaction --no-ansi && \
|
||||
poetry cache clear . --all && \
|
||||
poetry cache clear . --all --no-interaction && \
|
||||
rm -rf /root/.cache/
|
||||
|
||||
# Cleanup
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ from mobsf.install.windows.setup import windows_config_local
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
VERSION = '4.3.1'
|
||||
VERSION = '4.3.2'
|
||||
BANNER = r"""
|
||||
__ __ _ ____ _____ _ _ _____
|
||||
| \/ | ___ | |__/ ___|| ___|_ _| || | |___ /
|
||||
|
||||
@@ -3,10 +3,13 @@ import subprocess
|
||||
import functools
|
||||
import logging
|
||||
import re
|
||||
import socket
|
||||
import ipaddress
|
||||
import sys
|
||||
from shutil import which
|
||||
from pathlib import Path
|
||||
from platform import system
|
||||
from urllib.parse import urlparse
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from mobsf.MobSF.utils import (
|
||||
@@ -249,3 +252,77 @@ def sanitize_for_logging(filename: str, max_length: int = 255) -> str:
|
||||
|
||||
# Truncate filename to the maximum allowed length
|
||||
return filename[:max_length]
|
||||
|
||||
|
||||
def valid_host(host):
|
||||
"""Check if host is valid, run SSRF checks."""
|
||||
default_timeout = socket.getdefaulttimeout()
|
||||
try:
|
||||
if len(host) > 2083: # Standard URL length limit
|
||||
return False
|
||||
|
||||
prefixs = ('http://', 'https://')
|
||||
if not host.startswith(prefixs):
|
||||
host = f'http://{host}'
|
||||
parsed = urlparse(host)
|
||||
scheme = parsed.scheme
|
||||
domain = parsed.netloc
|
||||
hostname = parsed.hostname
|
||||
path = parsed.path
|
||||
query = parsed.query
|
||||
params = parsed.params
|
||||
port = parsed.port
|
||||
|
||||
# Allow only http and https schemes
|
||||
if scheme not in ('http', 'https'):
|
||||
return False
|
||||
|
||||
# Check for hostname
|
||||
if not hostname:
|
||||
return False
|
||||
|
||||
# Validate port - only allow 80 and 443
|
||||
if port and port not in (80, 443):
|
||||
return False
|
||||
|
||||
# Check for URL credentials
|
||||
if '@' in domain:
|
||||
return False
|
||||
|
||||
# Detect parser escapes, only host is allowed
|
||||
if len(path) > 0 or len(query) > 0 or len(params) > 0:
|
||||
return False
|
||||
|
||||
# Resolve dns to get ipv4 or ipv6 address
|
||||
socket.setdefaulttimeout(5) # 5 second timeout
|
||||
ip_addresses = socket.getaddrinfo(hostname, None)
|
||||
for ip in ip_addresses:
|
||||
ip_obj = ipaddress.ip_address(ip[4][0])
|
||||
if (ip_obj.is_private
|
||||
or ip_obj.is_loopback
|
||||
or ip_obj.is_link_local
|
||||
or ip_obj.is_reserved
|
||||
or ip_obj.is_multicast
|
||||
or ip_obj.is_unspecified):
|
||||
return False
|
||||
|
||||
# Additional checks for specific IPv4 ranges
|
||||
if isinstance(ip_obj, ipaddress.IPv4Address):
|
||||
problematic_networks = [
|
||||
'127.0.0.0/8',
|
||||
'169.254.0.0/16',
|
||||
'172.16.0.0/12',
|
||||
'192.168.0.0/16',
|
||||
'10.0.0.0/8',
|
||||
]
|
||||
for network in problematic_networks:
|
||||
if ip_obj in ipaddress.IPv4Network(network):
|
||||
return False
|
||||
|
||||
# If all checks pass, return True
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
finally:
|
||||
# Restore default socket timeout
|
||||
socket.setdefaulttimeout(default_timeout)
|
||||
|
||||
@@ -16,11 +16,9 @@ import signal
|
||||
import string
|
||||
import subprocess
|
||||
import stat
|
||||
import socket
|
||||
import sqlite3
|
||||
import unicodedata
|
||||
import threading
|
||||
from urllib.parse import urlparse
|
||||
from pathlib import Path
|
||||
from concurrent.futures import (
|
||||
ThreadPoolExecutor,
|
||||
@@ -904,58 +902,6 @@ def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
|
||||
return ''.join(random.choice(chars) for _ in range(size))
|
||||
|
||||
|
||||
def valid_host(host):
|
||||
"""Check if host is valid."""
|
||||
try:
|
||||
prefixs = ('http://', 'https://')
|
||||
if not host.startswith(prefixs):
|
||||
host = f'http://{host}'
|
||||
parsed = urlparse(host)
|
||||
domain = parsed.netloc
|
||||
path = parsed.path
|
||||
if len(domain) == 0:
|
||||
# No valid domain
|
||||
return False
|
||||
if len(path) > 0:
|
||||
# Only host is allowed
|
||||
return False
|
||||
if ':' in domain:
|
||||
# IPv6
|
||||
return False
|
||||
# Local network
|
||||
invalid_prefix = (
|
||||
'100.64.',
|
||||
'127.',
|
||||
'192.',
|
||||
'198.',
|
||||
'10.',
|
||||
'172.',
|
||||
'169.',
|
||||
'0.',
|
||||
'203.0.',
|
||||
'224.0.',
|
||||
'240.0',
|
||||
'255.255.',
|
||||
'localhost',
|
||||
'::1',
|
||||
'64::ff9b::',
|
||||
'100::',
|
||||
'2001::',
|
||||
'2002::',
|
||||
'fc00::',
|
||||
'fe80::',
|
||||
'ff00::')
|
||||
if domain.startswith(invalid_prefix):
|
||||
return False
|
||||
ip = socket.gethostbyname(domain)
|
||||
if ip.startswith(invalid_prefix):
|
||||
# Resolve dns to get IP
|
||||
return False
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def append_scan_status(checksum, status, exception=None):
|
||||
"""Append Scan Status to Database."""
|
||||
try:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# flake8: noqa
|
||||
"""Module for android manifest analysis."""
|
||||
import logging
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
@@ -10,8 +11,8 @@ from mobsf.MobSF.utils import (
|
||||
append_scan_status,
|
||||
is_number,
|
||||
upstream_proxy,
|
||||
valid_host,
|
||||
)
|
||||
from mobsf.MobSF.security import valid_host
|
||||
from mobsf.StaticAnalyzer.views.android import (
|
||||
network_security,
|
||||
)
|
||||
@@ -27,6 +28,8 @@ ANDROID_8_0_LEVEL = 26
|
||||
ANDROID_9_0_LEVEL = 28
|
||||
ANDROID_10_0_LEVEL = 29
|
||||
ANDROID_MANIFEST_FILE = 'AndroidManifest.xml'
|
||||
WELL_KNOWN_PATH = '/.well-known/assetlinks.json'
|
||||
|
||||
ANDROID_API_LEVEL_MAP = {
|
||||
'1': '1.0',
|
||||
'2': '1.1',
|
||||
@@ -96,6 +99,14 @@ 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
|
||||
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
|
||||
r = requests.get(url,
|
||||
timeout=5,
|
||||
allow_redirects=False,
|
||||
@@ -134,7 +145,6 @@ def get_browsable_activities(node, ns):
|
||||
path_prefixs = []
|
||||
path_patterns = []
|
||||
well_known = {}
|
||||
well_known_path = '/.well-known/assetlinks.json'
|
||||
catg = node.getElementsByTagName('category')
|
||||
for cat in catg:
|
||||
if cat.getAttribute(f'{ns}:name') == 'android.intent.category.BROWSABLE':
|
||||
@@ -168,12 +178,13 @@ def get_browsable_activities(node, ns):
|
||||
and host != '*'):
|
||||
host = host.replace('*.', '').replace('#', '')
|
||||
if not valid_host(host):
|
||||
logger.warning('Invalid Host: %s', host)
|
||||
continue
|
||||
shost = f'{scheme}://{host}'
|
||||
if port and is_number(port):
|
||||
c_url = f'{shost}:{port}{well_known_path}'
|
||||
c_url = f'{shost}:{port}{WELL_KNOWN_PATH}'
|
||||
else:
|
||||
c_url = f'{shost}{well_known_path}'
|
||||
c_url = f'{shost}{WELL_KNOWN_PATH}'
|
||||
well_known[c_url] = shost
|
||||
schemes = [scheme + '://' for scheme in schemes]
|
||||
browse_dic['schemes'] = schemes
|
||||
|
||||
@@ -5,8 +5,8 @@ from urllib.parse import urlparse
|
||||
from mobsf.MobSF.utils import (
|
||||
append_scan_status,
|
||||
upstream_proxy,
|
||||
valid_host,
|
||||
)
|
||||
from mobsf.MobSF.security import valid_host
|
||||
|
||||
import requests
|
||||
|
||||
@@ -80,13 +80,12 @@ def firebase_analysis(checksum, code_an_dic):
|
||||
def open_firebase(checksum, url):
|
||||
# Detect Open Firebase Database
|
||||
try:
|
||||
invalid = 'Invalid Firebase URL'
|
||||
if not valid_host(url):
|
||||
logger.warning(invalid)
|
||||
logger.warning('Invalid Host: %s', url)
|
||||
return url, False
|
||||
purl = urlparse(url)
|
||||
if not purl.netloc.endswith('.firebaseio.com'):
|
||||
logger.warning(invalid)
|
||||
if not purl.netloc.lower().endswith('.firebaseio.com'):
|
||||
logger.warning('Invalid Firebase URL')
|
||||
return url, False
|
||||
base_url = f'{purl.scheme}://{purl.netloc}/.json'
|
||||
proxies, verify = upstream_proxy('https')
|
||||
|
||||
Generated
+19
-19
@@ -801,24 +801,24 @@ dotenv = ["python-dotenv"]
|
||||
|
||||
[[package]]
|
||||
name = "frida"
|
||||
version = "16.7.0"
|
||||
version = "16.7.3"
|
||||
description = "Dynamic instrumentation toolkit for developers, reverse-engineers, and security researchers"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "frida-16.7.0-cp37-abi3-macosx_10_13_x86_64.whl", hash = "sha256:5f89f5a0143b5c0ed6e9ea6357ab0a948cf9ff4da650c3a14b7d49b26ff67eae"},
|
||||
{file = "frida-16.7.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:459fe6f876e075c1a0a7d3f5f577207f2531f24700eed6cb3c4494b1089f9629"},
|
||||
{file = "frida-16.7.0-cp37-abi3-manylinux1_i686.whl", hash = "sha256:e85c5112d866b412b7ad079b5f2cf8869187a535f35bcfafdf59f25b6a4f2df9"},
|
||||
{file = "frida-16.7.0-cp37-abi3-manylinux1_x86_64.whl", hash = "sha256:d28437747f9bb2624be4c97ffc15912ff5c3d0279227ad1024e20485525c49fd"},
|
||||
{file = "frida-16.7.0-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:d65656ab3f393ecacb491f15284db962ce35875a5dba3dc658c4715de886949d"},
|
||||
{file = "frida-16.7.0-cp37-abi3-manylinux2014_armv7l.whl", hash = "sha256:415afa2435c530fb416c156edeb870340809a54e6fd21b0e49c7e3e440377cde"},
|
||||
{file = "frida-16.7.0-cp37-abi3-manylinux_2_17_aarch64.whl", hash = "sha256:d854158411644e620a915209adff5cb0291ab42704f0abecdb9807132ec8c2dc"},
|
||||
{file = "frida-16.7.0-cp37-abi3-manylinux_2_17_armv7l.whl", hash = "sha256:42e46321de58faaf2bd2b23faddbd33133d02f73c9d946ce87bbdb66aa831e83"},
|
||||
{file = "frida-16.7.0-cp37-abi3-manylinux_2_5_i686.whl", hash = "sha256:2674f36a5ef9218e116e5a92eb37fd444f31b7db3f9d3615ac17c8ab21ba8281"},
|
||||
{file = "frida-16.7.0-cp37-abi3-manylinux_2_5_x86_64.whl", hash = "sha256:fdc3b59a2ad452b1fb52a4c651cef1140ce9facbd09ba6ff411aa2122d1424a1"},
|
||||
{file = "frida-16.7.0-cp37-abi3-win32.whl", hash = "sha256:c3ed99c47752a3c945cbaac71b662199eb5d0dc9fc9d0ba0e7b932896e35e946"},
|
||||
{file = "frida-16.7.0-cp37-abi3-win_amd64.whl", hash = "sha256:735ab69be575e3c9cfd7e3af2daa52423ea1636073bc28657414c8bc2cefba09"},
|
||||
{file = "frida-16.7.0.tar.gz", hash = "sha256:2ffa6ee671ba17ba51ad347b451d6f5ad392d86dc0d008741cac93a8ec278f7f"},
|
||||
{file = "frida-16.7.3-cp37-abi3-macosx_10_13_x86_64.whl", hash = "sha256:1eac812a7f96efdc9b92d2fc0591f6409d26d16b11d0789661d86d7a00a62425"},
|
||||
{file = "frida-16.7.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:6b637e2e5991ab5ec5a4f367e1c2ffcabbadb578c7a96027e3fb93e9a10ef95e"},
|
||||
{file = "frida-16.7.3-cp37-abi3-manylinux1_i686.whl", hash = "sha256:fabee6ad6cce7b6e5034f24818ffe8c0c5aebdd98a3635a8e343b8275705639e"},
|
||||
{file = "frida-16.7.3-cp37-abi3-manylinux1_x86_64.whl", hash = "sha256:896c17f5127a2355af83ac46edf2e1f54c734655d654db0bc20c43413074745f"},
|
||||
{file = "frida-16.7.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:005822c9501894ecb7ca7b8e91774c64ca08a3aebbb0c6c6d20c154556bdc2d9"},
|
||||
{file = "frida-16.7.3-cp37-abi3-manylinux2014_armv7l.whl", hash = "sha256:1b41f61badac532a23a01e903c3c8df8c1c01cfa7efc014325787d62111947c9"},
|
||||
{file = "frida-16.7.3-cp37-abi3-manylinux_2_17_aarch64.whl", hash = "sha256:8608b1df718589a60122ecbed86b38055705b232396981c3dfa2bba571e621ba"},
|
||||
{file = "frida-16.7.3-cp37-abi3-manylinux_2_17_armv7l.whl", hash = "sha256:68dade3e374bedf04558c7e539d51701fbfc797862deaf189ccdc59a8bc92438"},
|
||||
{file = "frida-16.7.3-cp37-abi3-manylinux_2_5_i686.whl", hash = "sha256:9a2748a116cb0dcf3f7cfaccb0b2f68dab60c47c4b265af77e8750f14a0969c0"},
|
||||
{file = "frida-16.7.3-cp37-abi3-manylinux_2_5_x86_64.whl", hash = "sha256:eee00ae13342a7f6a5adbb2273404f6cf9ac3d0cf1ee0501ebca31ff4fdf2577"},
|
||||
{file = "frida-16.7.3-cp37-abi3-win32.whl", hash = "sha256:8bdc8155e1117dce6bd4989e781250fa7b7de268df86c365c2c6361c5f066692"},
|
||||
{file = "frida-16.7.3-cp37-abi3-win_amd64.whl", hash = "sha256:2d074ec6eb7c25d0fc9ebe3b1d31965ad324428a4c05fcb2eb993fcf4c1d4f22"},
|
||||
{file = "frida-16.7.3.tar.gz", hash = "sha256:15b6816a00a83c5f08f10a8070f092f65e444e9708503990964cb2ff933139bf"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1687,17 +1687,17 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1-modules"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
description = "A collection of ASN.1-based protocols modules"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"},
|
||||
{file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"},
|
||||
{file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"},
|
||||
{file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pyasn1 = ">=0.4.6,<0.7.0"
|
||||
pyasn1 = ">=0.6.1,<0.7.0"
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
@@ -2703,4 +2703,4 @@ cffi = ["cffi (>=1.11)"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "63c7df876bbf9f274e0077b626363e6a2e2d726dc7178c2ec4c3e886bb564c0d"
|
||||
content-hash = "3911da9d90dafdd20f5797ef37b8849a164aede1d22d3dc77d401700ca55515f"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "mobsf"
|
||||
version = "4.3.1"
|
||||
version = "4.3.2"
|
||||
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]>"]
|
||||
@@ -31,7 +31,7 @@ requests = ">=2.25.1"
|
||||
bs4 = ">=0.0.1"
|
||||
colorlog = ">=4.7.2"
|
||||
macholib = ">=1.14"
|
||||
whitenoise = ">=5.2.0"
|
||||
whitenoise = ">=6.8.2"
|
||||
waitress = {version = ">=3.0.1", markers = "sys_platform == 'win32'"}
|
||||
gunicorn = {version = ">=20.0.4", markers = "sys_platform != 'win32'"}
|
||||
psutil = ">=5.8.0"
|
||||
|
||||
Reference in New Issue
Block a user