mirror of
https://github.com/MobSF/Mobile-Security-Framework-MobSF.git
synced 2026-10-12 02:06:59 +00:00
Detect known secret formats in Android/iOS string analysis (#2640)
Add a shared high-confidence secret detector (secret_detection.py) for known credential formats. Integrate it into Android/iOS string, resource, binary, and plist secret extraction. Keep labeled known secrets at the top of Hardcoded Secrets reports. Remove Amazon LWA rules from android_rules.yaml so SAST stays focused on code-level issues. Co-authored-by: Ajin Abraham <[email protected]> Co-authored-by: Cursor <[email protected]>
This commit is contained in:
co-authored by
Ajin Abraham
Cursor
parent
a20d60bf12
commit
e41d49bd0d
@@ -13,6 +13,9 @@ from mobsf.MobSF.utils import (
|
||||
from mobsf.MobSF.views.home import update_scan_timestamp
|
||||
from mobsf.StaticAnalyzer.models import StaticAnalyzerAndroid
|
||||
from mobsf.StaticAnalyzer.models import RecentScansDB
|
||||
from mobsf.StaticAnalyzer.views.common.secret_detection import (
|
||||
sort_secrets,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.suppression import (
|
||||
process_suppression,
|
||||
process_suppression_manifest,
|
||||
@@ -86,7 +89,8 @@ def get_context_from_db_entry(db_entry: QuerySet) -> dict:
|
||||
'behaviour': python_dict(db_entry[0].QUARK),
|
||||
'trackers': python_dict(db_entry[0].TRACKERS),
|
||||
'playstore_details': python_dict(db_entry[0].PLAYSTORE_DETAILS),
|
||||
'secrets': python_list(db_entry[0].SECRETS),
|
||||
'secrets': sort_secrets(
|
||||
python_list(db_entry[0].SECRETS)),
|
||||
'logs': get_scan_logs(db_entry[0].MD5),
|
||||
'sbom': python_dict(db_entry[0].SBOM),
|
||||
}
|
||||
@@ -161,7 +165,7 @@ def get_context_from_analysis(app_dic,
|
||||
'behaviour': code_an_dic['behaviour'],
|
||||
'trackers': trackers,
|
||||
'playstore_details': app_dic['playstore'],
|
||||
'secrets': code_an_dic['secrets'],
|
||||
'secrets': sort_secrets(code_an_dic['secrets']),
|
||||
'logs': get_scan_logs(app_dic['md5']),
|
||||
'sbom': code_an_dic['sbom'],
|
||||
}
|
||||
|
||||
@@ -8,8 +8,9 @@ from mobsf.StaticAnalyzer.views.common.shared_func import (
|
||||
strings_and_entropies,
|
||||
url_n_email_extract,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.entropy import (
|
||||
get_entropies,
|
||||
from mobsf.StaticAnalyzer.views.common.secret_detection import (
|
||||
detect_known_secrets,
|
||||
get_secrets,
|
||||
)
|
||||
from mobsf.MobSF.utils import (
|
||||
GOOGLE_API_KEY_REGEX,
|
||||
@@ -37,7 +38,7 @@ def strings_from_so(checksum, elf_strings):
|
||||
so_urls, so_urls_nf, so_emails_nf = url_n_email_extract(
|
||||
so_str, so)
|
||||
sos.append({so: {
|
||||
'secrets': list(get_entropies(so_str)),
|
||||
'secrets': list(get_secrets(str_list)),
|
||||
'strings': list(set(str_list)),
|
||||
'urls_list': so_urls,
|
||||
'urls_nf': so_urls_nf,
|
||||
@@ -93,9 +94,12 @@ def strings_from_apk(checksum, app_dic):
|
||||
# Check for possible secrets
|
||||
if is_secret_key(key) and ' ' not in value:
|
||||
results['secrets'].append(formatted_str)
|
||||
results['secrets'].extend(
|
||||
detect_known_secrets(value))
|
||||
elif app_dic.get('apk_strings'):
|
||||
# No secret key check for APK strings
|
||||
results['strings'] = app_dic['apk_strings']
|
||||
results['secrets'].extend(
|
||||
detect_known_secrets(results['strings']))
|
||||
else:
|
||||
msg = 'Failed to extract String data from APK'
|
||||
logger.warning(msg)
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
"""Known secret pattern detection."""
|
||||
import re
|
||||
|
||||
from mobsf.StaticAnalyzer.views.common.entropy import (
|
||||
get_entropies,
|
||||
)
|
||||
|
||||
KNOWN_SECRET_PATTERNS = (
|
||||
(
|
||||
'GitHub token',
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9_])'
|
||||
r'gh[pousr]_[A-Za-z0-9]{36,255}'
|
||||
r'(?![A-Za-z0-9])'),
|
||||
),
|
||||
(
|
||||
'GitHub fine-grained token',
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9_])'
|
||||
r'github_pat_[A-Za-z0-9_]{20,255}'
|
||||
r'(?![A-Za-z0-9_])'),
|
||||
),
|
||||
(
|
||||
'GitLab personal access token',
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9_-])'
|
||||
r'glpat-[A-Za-z0-9_-]{20,255}'
|
||||
r'(?![A-Za-z0-9_-])'),
|
||||
),
|
||||
(
|
||||
'Slack token',
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9-])'
|
||||
r'xox[baprs]-[A-Za-z0-9-]{10,255}'
|
||||
r'(?![A-Za-z0-9-])'),
|
||||
),
|
||||
(
|
||||
'Slack webhook',
|
||||
re.compile(
|
||||
r'https://hooks\.slack\.com/services/'
|
||||
r'T[A-Z0-9]{8,12}/B[A-Z0-9]{8,12}/'
|
||||
r'[A-Za-z0-9]{20,64}'
|
||||
r'(?![A-Za-z0-9])'),
|
||||
),
|
||||
(
|
||||
'Stripe live secret key',
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9_])'
|
||||
r'(?:sk|rk)_live_[A-Za-z0-9]{16,255}'
|
||||
r'(?![A-Za-z0-9])'),
|
||||
),
|
||||
(
|
||||
'SendGrid API key',
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9_.-])'
|
||||
r'SG\.[A-Za-z0-9_-]{16,64}\.'
|
||||
r'[A-Za-z0-9_-]{16,128}'
|
||||
r'(?![A-Za-z0-9_-])'),
|
||||
),
|
||||
(
|
||||
'npm access token',
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9_])'
|
||||
r'npm_[A-Za-z0-9]{36,64}'
|
||||
r'(?![A-Za-z0-9])'),
|
||||
),
|
||||
(
|
||||
'Mapbox secret token',
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9_.-])'
|
||||
r'sk\.[A-Za-z0-9_-]{20,256}\.'
|
||||
r'[A-Za-z0-9_-]{20,256}'
|
||||
r'(?![A-Za-z0-9_-])'),
|
||||
),
|
||||
(
|
||||
'Anthropic API key',
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9_-])'
|
||||
r'sk-ant-(?:api03|admin01)-'
|
||||
r'[A-Za-z0-9_-]{93}AA'
|
||||
r'(?![A-Za-z0-9_-])'),
|
||||
),
|
||||
(
|
||||
'OpenAI API key',
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9_-])'
|
||||
r'(?:'
|
||||
r'sk-(?:proj|svcacct|admin)-'
|
||||
r'(?:[A-Za-z0-9_-]{58}|[A-Za-z0-9_-]{74})'
|
||||
r'T3BlbkFJ'
|
||||
r'(?:[A-Za-z0-9_-]{58}|[A-Za-z0-9_-]{74})'
|
||||
r'|sk-[A-Za-z0-9]{20}T3BlbkFJ[A-Za-z0-9]{20}'
|
||||
r')'
|
||||
r'(?![A-Za-z0-9_-])'),
|
||||
),
|
||||
(
|
||||
'Hugging Face access token',
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9_])'
|
||||
r'hf_[A-Za-z]{34}'
|
||||
r'(?![A-Za-z0-9])'),
|
||||
),
|
||||
(
|
||||
'DigitalOcean token',
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9_])'
|
||||
r'(?:doo|dop|dor)_v1_[a-f0-9]{64}'
|
||||
r'(?![A-Za-z0-9])'),
|
||||
),
|
||||
(
|
||||
'Doppler API token',
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9_.])'
|
||||
r'dp\.pt\.[A-Za-z0-9]{43}'
|
||||
r'(?![A-Za-z0-9])'),
|
||||
),
|
||||
(
|
||||
'Brevo API token',
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9_-])'
|
||||
r'xkeysib-[a-f0-9]{64}-[A-Za-z0-9]{16}'
|
||||
r'(?![A-Za-z0-9])'),
|
||||
),
|
||||
(
|
||||
'Sentry user token',
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9_])'
|
||||
r'sntryu_[a-f0-9]{64}'
|
||||
r'(?![A-Za-z0-9])'),
|
||||
),
|
||||
(
|
||||
'Shopify access token',
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9_])'
|
||||
r'(?:shpat|shpca|shppa|shpss)_[A-Fa-f0-9]{32}'
|
||||
r'(?![A-Fa-f0-9])'),
|
||||
),
|
||||
(
|
||||
'Square access token',
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9_-])'
|
||||
r'sq0atp-[A-Za-z0-9_-]{22,60}'
|
||||
r'(?![A-Za-z0-9_-])'),
|
||||
),
|
||||
(
|
||||
'Private key',
|
||||
re.compile(
|
||||
r'-----BEGIN '
|
||||
r'(?:RSA |EC |DSA |OPENSSH )?'
|
||||
r'PRIVATE KEY-----'),
|
||||
),
|
||||
(
|
||||
'Amazon LWA client secret',
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9_.-])'
|
||||
r'amzn1\.oa2-cs\.v[0-9]+\.[0-9A-Fa-f]{32,128}'
|
||||
r'(?![0-9A-Fa-f])'),
|
||||
),
|
||||
(
|
||||
'Amazon LWA access token',
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9_-])'
|
||||
r'Atza\|[A-Za-z0-9_-]{20,2043}'
|
||||
r'(?![A-Za-z0-9_-])'),
|
||||
),
|
||||
(
|
||||
'Amazon LWA refresh token',
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9_-])'
|
||||
r'Atzr\|[A-Za-z0-9_-]{20,2043}'
|
||||
r'(?![A-Za-z0-9_-])'),
|
||||
),
|
||||
)
|
||||
KNOWN_SECRET_PREFIXES = tuple(
|
||||
f'{name}: ' for name, _pattern in KNOWN_SECRET_PATTERNS)
|
||||
|
||||
|
||||
def _strings(data):
|
||||
"""Normalize a string or iterable of strings to a tuple."""
|
||||
if data is None:
|
||||
return ()
|
||||
if isinstance(data, str):
|
||||
return (data,)
|
||||
return tuple(data)
|
||||
|
||||
|
||||
def detect_known_secrets(data):
|
||||
"""Detect and label secrets with known value formats."""
|
||||
secrets = set()
|
||||
for value in _strings(data):
|
||||
if not isinstance(value, str):
|
||||
continue
|
||||
for name, pattern in KNOWN_SECRET_PATTERNS:
|
||||
for match in pattern.finditer(value):
|
||||
secrets.add(f'{name}: {match.group()}')
|
||||
return secrets
|
||||
|
||||
|
||||
def get_secrets(data):
|
||||
"""Detect known-format and high-entropy secrets."""
|
||||
secrets = set()
|
||||
for value in _strings(data):
|
||||
if not isinstance(value, str):
|
||||
continue
|
||||
scrubbed = value
|
||||
for name, pattern in KNOWN_SECRET_PATTERNS:
|
||||
for match in pattern.finditer(value):
|
||||
secrets.add(f'{name}: {match.group()}')
|
||||
scrubbed = pattern.sub('', scrubbed)
|
||||
secrets.update(get_entropies((scrubbed,)))
|
||||
return secrets
|
||||
|
||||
|
||||
def sort_secrets(secrets):
|
||||
"""Place labeled known-format secrets before heuristic findings."""
|
||||
return sorted(
|
||||
set(secrets),
|
||||
key=lambda secret: (
|
||||
not secret.startswith(KNOWN_SECRET_PREFIXES),
|
||||
secret.casefold(),
|
||||
),
|
||||
)
|
||||
@@ -43,8 +43,8 @@ from mobsf.MobSF.views.scanning import (
|
||||
from mobsf.StaticAnalyzer.views.comparer import (
|
||||
generic_compare,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.entropy import (
|
||||
get_entropies,
|
||||
from mobsf.StaticAnalyzer.views.common.secret_detection import (
|
||||
get_secrets,
|
||||
)
|
||||
from mobsf.MobSF.views.authentication import (
|
||||
login_required,
|
||||
@@ -494,7 +494,7 @@ def strings_and_entropies(checksum, src, exts):
|
||||
continue
|
||||
data['strings'].add(string)
|
||||
if data['strings']:
|
||||
data['secrets'] = get_entropies(data['strings'])
|
||||
data['secrets'] = get_secrets(data['strings'])
|
||||
except Exception as exp:
|
||||
msg = 'Failed to extract String values and entropies from Code'
|
||||
logger.exception(msg)
|
||||
|
||||
@@ -12,6 +12,9 @@ from mobsf.MobSF.utils import (
|
||||
from mobsf.MobSF.views.home import update_scan_timestamp
|
||||
from mobsf.StaticAnalyzer.models import StaticAnalyzerIOS
|
||||
from mobsf.StaticAnalyzer.models import RecentScansDB
|
||||
from mobsf.StaticAnalyzer.views.common.secret_detection import (
|
||||
sort_secrets,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.suppression import (
|
||||
process_suppression,
|
||||
)
|
||||
@@ -70,7 +73,8 @@ def get_context_from_db_entry(db_entry):
|
||||
'strings': python_list(db_entry[0].STRINGS),
|
||||
'firebase_urls': python_list(db_entry[0].FIREBASE_URLS),
|
||||
'appstore_details': python_dict(db_entry[0].APPSTORE_DETAILS),
|
||||
'secrets': python_list(db_entry[0].SECRETS),
|
||||
'secrets': sort_secrets(
|
||||
python_list(db_entry[0].SECRETS)),
|
||||
'trackers': python_dict(db_entry[0].TRACKERS),
|
||||
'logs': get_scan_logs(db_entry[0].MD5),
|
||||
}
|
||||
@@ -134,7 +138,7 @@ def get_context_from_analysis(app_dict,
|
||||
'strings': bin_dict['strings'],
|
||||
'firebase_urls': code_dict['firebase'],
|
||||
'appstore_details': app_dict['appstore'],
|
||||
'secrets': app_dict['secrets'],
|
||||
'secrets': sort_secrets(app_dict['secrets']),
|
||||
'trackers': code_dict['trackers'],
|
||||
'logs': get_scan_logs(app_dict['md5_hash']),
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ from mobsf.StaticAnalyzer.views.ios.kb.permission_analysis import (
|
||||
from mobsf.StaticAnalyzer.views.ios.app_transport_security import (
|
||||
check_transport_security,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.secret_detection import (
|
||||
detect_known_secrets,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.shared_func import (
|
||||
is_secret_key,
|
||||
)
|
||||
@@ -247,6 +250,7 @@ def get_plist_secrets(checksum, app_dir):
|
||||
|
||||
for i in Path(app_dir).rglob('*.plist'):
|
||||
xml_string = i.read_text('utf-8', 'ignore')
|
||||
result_list.update(detect_known_secrets(xml_string))
|
||||
xml_list = xml_string.split('\n')
|
||||
for index, line in enumerate(xml_list):
|
||||
if '<key>' in line and is_secret_key(_remove_tags(line)):
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import io
|
||||
import logging
|
||||
|
||||
from mobsf.StaticAnalyzer.views.common.entropy import (
|
||||
get_entropies,
|
||||
from mobsf.StaticAnalyzer.views.common.secret_detection import (
|
||||
get_secrets,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.shared_func import (
|
||||
url_n_email_extract,
|
||||
@@ -73,7 +73,7 @@ def get_strings_metadata(app_dict, bin_dict, all_files, dy_list):
|
||||
urls_list = []
|
||||
urls_n_files = []
|
||||
emails_n_files = []
|
||||
secrets = []
|
||||
secrets = list(get_secrets(bin_dict['strings']))
|
||||
|
||||
# IPA URL and Email Extract
|
||||
str_meta = extract_urls_n_email(
|
||||
@@ -97,7 +97,7 @@ def get_strings_metadata(app_dict, bin_dict, all_files, dy_list):
|
||||
urls_list.extend(urls)
|
||||
urls_n_files.extend(urls_nf)
|
||||
emails_n_files.extend(emails_nf)
|
||||
secrets.extend(get_entropies(dy_str))
|
||||
secrets.extend(get_secrets(s))
|
||||
|
||||
bin_dict['strings'].extend(dy_strings)
|
||||
bin_dict['strings'] = list(
|
||||
|
||||
Reference in New Issue
Block a user