[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:
Ajin Abraham
2025-03-28 17:38:23 -07:00
committed by GitHub
parent 506434670a
commit 4b8bab5a98
9 changed files with 121 additions and 87 deletions
+2 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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"""
__ __ _ ____ _____ _ _ _____
| \/ | ___ | |__/ ___|| ___|_ _| || | |___ /
+77
View File
@@ -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)
-54
View File
@@ -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
View File
@@ -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
View File
@@ -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"