Skip to content

Commit 4b8bab5

Browse files
authored
[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
1 parent 5064346 commit 4b8bab5

File tree

9 files changed

+121
-87
lines changed

9 files changed

+121
-87
lines changed

.github/SECURITY.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ Keeping MobSF updated to the latest version is essential for ensuring security a
44

55
## Reporting a Vulnerability
66

7-
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.
7+
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.
88

99
## Past Security Issues
1010

1111
| Vulnerability | Affected Versions |
1212
| ------- | ------------------ |
13+
| [SSRF on assetlinks_check with DNS Rebinding](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-fcfq-m8p6-gw56) | `<=4.3.1` |
1314
| [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` |
1415
| [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` |
1516
| [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` |

Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ COPY pyproject.toml .
6464
RUN poetry config virtualenvs.create false && \
6565
poetry lock && \
6666
poetry install --only main --no-root --no-interaction --no-ansi && \
67-
poetry cache clear . --all && \
67+
poetry cache clear . --all --no-interaction && \
6868
rm -rf /root/.cache/
6969

7070
# Cleanup

mobsf/MobSF/init.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
logger = logging.getLogger(__name__)
2020

21-
VERSION = '4.3.1'
21+
VERSION = '4.3.2'
2222
BANNER = r"""
2323
__ __ _ ____ _____ _ _ _____
2424
| \/ | ___ | |__/ ___|| ___|_ _| || | |___ /

mobsf/MobSF/security.py

+77
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
import functools
44
import logging
55
import re
6+
import socket
7+
import ipaddress
68
import sys
79
from shutil import which
810
from pathlib import Path
911
from platform import system
12+
from urllib.parse import urlparse
1013
from concurrent.futures import ThreadPoolExecutor
1114

1215
from mobsf.MobSF.utils import (
@@ -249,3 +252,77 @@ def sanitize_for_logging(filename: str, max_length: int = 255) -> str:
249252

250253
# Truncate filename to the maximum allowed length
251254
return filename[:max_length]
255+
256+
257+
def valid_host(host):
258+
"""Check if host is valid, run SSRF checks."""
259+
default_timeout = socket.getdefaulttimeout()
260+
try:
261+
if len(host) > 2083: # Standard URL length limit
262+
return False
263+
264+
prefixs = ('http://', 'https://')
265+
if not host.startswith(prefixs):
266+
host = f'http://{host}'
267+
parsed = urlparse(host)
268+
scheme = parsed.scheme
269+
domain = parsed.netloc
270+
hostname = parsed.hostname
271+
path = parsed.path
272+
query = parsed.query
273+
params = parsed.params
274+
port = parsed.port
275+
276+
# Allow only http and https schemes
277+
if scheme not in ('http', 'https'):
278+
return False
279+
280+
# Check for hostname
281+
if not hostname:
282+
return False
283+
284+
# Validate port - only allow 80 and 443
285+
if port and port not in (80, 443):
286+
return False
287+
288+
# Check for URL credentials
289+
if '@' in domain:
290+
return False
291+
292+
# Detect parser escapes, only host is allowed
293+
if len(path) > 0 or len(query) > 0 or len(params) > 0:
294+
return False
295+
296+
# Resolve dns to get ipv4 or ipv6 address
297+
socket.setdefaulttimeout(5) # 5 second timeout
298+
ip_addresses = socket.getaddrinfo(hostname, None)
299+
for ip in ip_addresses:
300+
ip_obj = ipaddress.ip_address(ip[4][0])
301+
if (ip_obj.is_private
302+
or ip_obj.is_loopback
303+
or ip_obj.is_link_local
304+
or ip_obj.is_reserved
305+
or ip_obj.is_multicast
306+
or ip_obj.is_unspecified):
307+
return False
308+
309+
# Additional checks for specific IPv4 ranges
310+
if isinstance(ip_obj, ipaddress.IPv4Address):
311+
problematic_networks = [
312+
'127.0.0.0/8',
313+
'169.254.0.0/16',
314+
'172.16.0.0/12',
315+
'192.168.0.0/16',
316+
'10.0.0.0/8',
317+
]
318+
for network in problematic_networks:
319+
if ip_obj in ipaddress.IPv4Network(network):
320+
return False
321+
322+
# If all checks pass, return True
323+
return True
324+
except Exception:
325+
return False
326+
finally:
327+
# Restore default socket timeout
328+
socket.setdefaulttimeout(default_timeout)

mobsf/MobSF/utils.py

-54
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,9 @@
1616
import string
1717
import subprocess
1818
import stat
19-
import socket
2019
import sqlite3
2120
import unicodedata
2221
import threading
23-
from urllib.parse import urlparse
2422
from pathlib import Path
2523
from concurrent.futures import (
2624
ThreadPoolExecutor,
@@ -904,58 +902,6 @@ def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
904902
return ''.join(random.choice(chars) for _ in range(size))
905903

906904

907-
def valid_host(host):
908-
"""Check if host is valid."""
909-
try:
910-
prefixs = ('http://', 'https://')
911-
if not host.startswith(prefixs):
912-
host = f'http://{host}'
913-
parsed = urlparse(host)
914-
domain = parsed.netloc
915-
path = parsed.path
916-
if len(domain) == 0:
917-
# No valid domain
918-
return False
919-
if len(path) > 0:
920-
# Only host is allowed
921-
return False
922-
if ':' in domain:
923-
# IPv6
924-
return False
925-
# Local network
926-
invalid_prefix = (
927-
'100.64.',
928-
'127.',
929-
'192.',
930-
'198.',
931-
'10.',
932-
'172.',
933-
'169.',
934-
'0.',
935-
'203.0.',
936-
'224.0.',
937-
'240.0',
938-
'255.255.',
939-
'localhost',
940-
'::1',
941-
'64::ff9b::',
942-
'100::',
943-
'2001::',
944-
'2002::',
945-
'fc00::',
946-
'fe80::',
947-
'ff00::')
948-
if domain.startswith(invalid_prefix):
949-
return False
950-
ip = socket.gethostbyname(domain)
951-
if ip.startswith(invalid_prefix):
952-
# Resolve dns to get IP
953-
return False
954-
return True
955-
except Exception:
956-
return False
957-
958-
959905
def append_scan_status(checksum, status, exception=None):
960906
"""Append Scan Status to Database."""
961907
try:

mobsf/StaticAnalyzer/views/android/manifest_analysis.py

+15-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# flake8: noqa
33
"""Module for android manifest analysis."""
44
import logging
5+
from urllib.parse import urlparse
56

67
import requests
78
from concurrent.futures import ThreadPoolExecutor
@@ -10,8 +11,8 @@
1011
append_scan_status,
1112
is_number,
1213
upstream_proxy,
13-
valid_host,
1414
)
15+
from mobsf.MobSF.security import valid_host
1516
from mobsf.StaticAnalyzer.views.android import (
1617
network_security,
1718
)
@@ -27,6 +28,8 @@
2728
ANDROID_9_0_LEVEL = 28
2829
ANDROID_10_0_LEVEL = 29
2930
ANDROID_MANIFEST_FILE = 'AndroidManifest.xml'
31+
WELL_KNOWN_PATH = '/.well-known/assetlinks.json'
32+
3033
ANDROID_API_LEVEL_MAP = {
3134
'1': '1.0',
3235
'2': '1.1',
@@ -96,6 +99,14 @@ def _check_url(host, w_url):
9699
urls.add(f'https://{w_url[7:]}')
97100

98101
for url in urls:
102+
# Additional checks to ensure that
103+
# the final path is WELL_KNOWN_PATH
104+
purl = urlparse(url)
105+
if (purl.path != WELL_KNOWN_PATH
106+
or len(purl.query) > 0
107+
or len(purl.params) > 0):
108+
logger.warning('Invalid Assetlinks URL: %s', url)
109+
continue
99110
r = requests.get(url,
100111
timeout=5,
101112
allow_redirects=False,
@@ -134,7 +145,6 @@ def get_browsable_activities(node, ns):
134145
path_prefixs = []
135146
path_patterns = []
136147
well_known = {}
137-
well_known_path = '/.well-known/assetlinks.json'
138148
catg = node.getElementsByTagName('category')
139149
for cat in catg:
140150
if cat.getAttribute(f'{ns}:name') == 'android.intent.category.BROWSABLE':
@@ -168,12 +178,13 @@ def get_browsable_activities(node, ns):
168178
and host != '*'):
169179
host = host.replace('*.', '').replace('#', '')
170180
if not valid_host(host):
181+
logger.warning('Invalid Host: %s', host)
171182
continue
172183
shost = f'{scheme}://{host}'
173184
if port and is_number(port):
174-
c_url = f'{shost}:{port}{well_known_path}'
185+
c_url = f'{shost}:{port}{WELL_KNOWN_PATH}'
175186
else:
176-
c_url = f'{shost}{well_known_path}'
187+
c_url = f'{shost}{WELL_KNOWN_PATH}'
177188
well_known[c_url] = shost
178189
schemes = [scheme + '://' for scheme in schemes]
179190
browse_dic['schemes'] = schemes

mobsf/StaticAnalyzer/views/common/firebase.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
from mobsf.MobSF.utils import (
66
append_scan_status,
77
upstream_proxy,
8-
valid_host,
98
)
9+
from mobsf.MobSF.security import valid_host
1010

1111
import requests
1212

@@ -80,13 +80,12 @@ def firebase_analysis(checksum, code_an_dic):
8080
def open_firebase(checksum, url):
8181
# Detect Open Firebase Database
8282
try:
83-
invalid = 'Invalid Firebase URL'
8483
if not valid_host(url):
85-
logger.warning(invalid)
84+
logger.warning('Invalid Host: %s', url)
8685
return url, False
8786
purl = urlparse(url)
88-
if not purl.netloc.endswith('.firebaseio.com'):
89-
logger.warning(invalid)
87+
if not purl.netloc.lower().endswith('.firebaseio.com'):
88+
logger.warning('Invalid Firebase URL')
9089
return url, False
9190
base_url = f'{purl.scheme}://{purl.netloc}/.json'
9291
proxies, verify = upstream_proxy('https')

poetry.lock

+19-19
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "mobsf"
3-
version = "4.3.1"
3+
version = "4.3.2"
44
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."
55
keywords = ["mobsf", "mobile security framework", "mobile security", "security tool", "static analysis", "dynamic analysis", "malware analysis"]
66
authors = ["Ajin Abraham <[email protected]>"]
@@ -31,7 +31,7 @@ requests = ">=2.25.1"
3131
bs4 = ">=0.0.1"
3232
colorlog = ">=4.7.2"
3333
macholib = ">=1.14"
34-
whitenoise = ">=5.2.0"
34+
whitenoise = ">=6.8.2"
3535
waitress = {version = ">=3.0.1", markers = "sys_platform == 'win32'"}
3636
gunicorn = {version = ">=20.0.4", markers = "sys_platform != 'win32'"}
3737
psutil = ">=5.8.0"

0 commit comments

Comments
 (0)