Skip to content

Commit f8767ad

Browse files
committed
Improve SSRF checks, strict path check for well_known_path
1 parent 5064346 commit f8767ad

File tree

6 files changed

+83
-65
lines changed

6 files changed

+83
-65
lines changed

mobsf/MobSF/init.py

Lines changed: 1 addition & 1 deletion
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

Lines changed: 62 additions & 0 deletions
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,62 @@ 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+
try:
260+
if len(host) > 2083: # Standard URL length limit
261+
return False
262+
263+
prefixs = ('http://', 'https://')
264+
if not host.startswith(prefixs):
265+
host = f'http://{host}'
266+
parsed = urlparse(host)
267+
domain = parsed.netloc
268+
hostname = parsed.hostname
269+
path = parsed.path
270+
query = parsed.query
271+
params = parsed.params
272+
273+
# Check for hostname
274+
if not hostname:
275+
return False
276+
277+
# Check for URL credentials
278+
if '@' in domain:
279+
return False
280+
281+
# Detect parser escapes, only host is allowed
282+
if len(path) > 0 or len(query) > 0 or len(params) > 0:
283+
return False
284+
285+
# Resolve dns to get ipv4 or ipv6 address
286+
ip_addresses = socket.getaddrinfo(hostname, None, timeout=5)
287+
for ip in ip_addresses:
288+
ip_obj = ipaddress.ip_address(ip[4][0])
289+
if (ip_obj.is_private
290+
or ip_obj.is_loopback
291+
or ip_obj.is_link_local
292+
or ip_obj.is_reserved
293+
or ip_obj.is_multicast
294+
or ip_obj.is_unspecified):
295+
return False
296+
297+
# Additional checks for specific IPv4 ranges
298+
if isinstance(ip_obj, ipaddress.IPv4Address):
299+
problematic_networks = [
300+
'127.0.0.0/8',
301+
'169.254.0.0/16',
302+
'172.16.0.0/12',
303+
'192.168.0.0/16',
304+
'10.0.0.0/8',
305+
]
306+
for network in problematic_networks:
307+
if ip_obj in ipaddress.IPv4Network(network):
308+
return False
309+
310+
# If all checks pass, return True
311+
return True
312+
except Exception:
313+
return False

mobsf/MobSF/utils.py

Lines changed: 0 additions & 54 deletions
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

Lines changed: 15 additions & 4 deletions
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

Lines changed: 4 additions & 5 deletions
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')

pyproject.toml

Lines changed: 1 addition & 1 deletion
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 <ajin@opensecurity.in>"]

0 commit comments

Comments
 (0)