Skip to content

Commit 6987a94

Browse files
authored
v4.3.3 Security Updates (#2518)
* Fix GHSA-mwfg-948f-2cc5 * stricter email case validation * Fix GHSA-c5vg-26p8-q8cr * Bump deps * Lint QA
1 parent cecec6e commit 6987a94

File tree

10 files changed

+279
-127
lines changed

10 files changed

+279
-127
lines changed

.github/SECURITY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Please report all security issues [here](https://github.com/MobSF/Mobile-Securit
1010

1111
| Vulnerability | Affected Versions |
1212
| ------- | ------------------ |
13+
| [Zip bomb Denial of Service (DoS) via Resource Exhaustion (Disk Space)](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-c5vg-26p8-q8cr) | `<=4.3.2` |
14+
| [Stored Cross Site Scripting (XSS) via malicious SVG app icon](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-mwfg-948f-2cc5) | `<=4.3.2` |
1315
| [SSRF on assetlinks_check with DNS Rebinding](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-fcfq-m8p6-gw56) | `<=4.3.1` |
1416
| [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` |
1517
| [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` |

.github/workflows/codeql-analysis.yml

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ on:
66
pull_request:
77
branches: [ "master" ]
88
schedule:
9-
- cron: '18 14 * * 3'
9+
- cron: '45 23 * * 4'
1010

1111
jobs:
1212
analyze:
1313
name: Analyze (${{ matrix.language }})
14-
runs-on: ubuntu-latest
14+
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
1515
permissions:
1616
# required for all workflows
1717
security-events: write
@@ -27,21 +27,59 @@ jobs:
2727
fail-fast: false
2828
matrix:
2929
include:
30+
- language: actions
31+
build-mode: none
32+
- language: javascript-typescript
33+
build-mode: none
3034
- language: python
3135
build-mode: none
32-
36+
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
37+
# Use `c-cpp` to analyze code written in C, C++ or both
38+
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
39+
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
40+
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
41+
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
42+
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
43+
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
3344
steps:
3445
- name: Checkout repository
3546
uses: actions/checkout@v4
3647

48+
# Add any setup steps before running the `github/codeql-action/init` action.
49+
# This includes steps like installing compilers or runtimes (`actions/setup-node`
50+
# or others). This is typically only required for manual builds.
51+
# - name: Setup runtime (example)
52+
# uses: actions/setup-example@v1
53+
3754
# Initializes the CodeQL tools for scanning.
3855
- name: Initialize CodeQL
3956
uses: github/codeql-action/init@v3
4057
with:
4158
languages: ${{ matrix.language }}
4259
build-mode: ${{ matrix.build-mode }}
43-
config-file: .github/codeql-config.yml
44-
60+
# If you wish to specify custom queries, you can do so here or in a config file.
61+
# By default, queries listed here will override any specified in a config file.
62+
# Prefix the list here with "+" to use these queries and those in the config file.
63+
64+
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
65+
# queries: security-extended,security-and-quality
66+
67+
# If the analyze step fails for one of the languages you are analyzing with
68+
# "We were unable to automatically build your code", modify the matrix above
69+
# to set the build mode to "manual" for that language. Then modify this step
70+
# to build your code.
71+
# ℹ️ Command-line programs to run using the OS shell.
72+
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
73+
- if: matrix.build-mode == 'manual'
74+
shell: bash
75+
run: |
76+
echo 'If you are using a "manual" build mode for one or more of the' \
77+
'languages you are analyzing, replace this with the commands to build' \
78+
'your code, for example:'
79+
echo ' make bootstrap'
80+
echo ' make release'
81+
exit 1
82+
4583
- name: Perform CodeQL Analysis
4684
uses: github/codeql-action/analyze@v3
4785
with:

mobsf/MobSF/forms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class RegisterForm(UserCreationForm):
4747

4848
def clean_email(self):
4949
email = self.cleaned_data.get('email')
50-
if User.objects.filter(email=email).exists():
50+
if User.objects.filter(email__iexact=email).exists():
5151
raise forms.ValidationError('Email already exists')
5252
return email
5353

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.2'
21+
VERSION = '4.3.3'
2222
BANNER = r"""
2323
__ __ _ ____ _____ _ _ _____
2424
| \/ | ___ | |__/ ___|| ___|_ _| || | |___ /

mobsf/MobSF/security.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,3 +325,40 @@ def valid_host(host):
325325
finally:
326326
# Restore default socket timeout
327327
socket.setdefaulttimeout(default_timeout)
328+
329+
330+
def sanitize_svg(svg_content):
331+
"""Sanitize SVG content to prevent XSS attacks."""
332+
logger.info('Sanitizing SVG contents')
333+
import bleach
334+
# Allow standard SVG tags and attributes, but remove scripts and event handlers
335+
safe_tags = [
336+
'svg', 'g', 'path', 'rect', 'circle', 'ellipse',
337+
'line', 'polyline', 'polygon', 'text', 'tspan',
338+
'defs', 'use', 'image', 'mask', 'clipPath',
339+
'filter', 'linearGradient', 'radialGradient', 'stop',
340+
]
341+
safe_attrs = {
342+
'*': ['id', 'class', 'transform', 'fill', 'stroke', 'stroke-width', 'opacity'],
343+
'svg': ['width', 'height', 'viewBox', 'xmlns', 'version'],
344+
'path': ['d'],
345+
'rect': ['x', 'y', 'width', 'height', 'rx', 'ry'],
346+
'circle': ['cx', 'cy', 'r'],
347+
'ellipse': ['cx', 'cy', 'rx', 'ry'],
348+
'line': ['x1', 'y1', 'x2', 'y2'],
349+
'polyline': ['points'],
350+
'polygon': ['points'],
351+
'text': ['x', 'y', 'font-family', 'font-size'],
352+
'image': ['x', 'y', 'width', 'height', 'href'],
353+
'use': ['x', 'y', 'width', 'height', 'href'],
354+
'filter': ['x', 'y', 'width', 'height', 'href'],
355+
'linearGradient': ['x1', 'y1', 'x2', 'y2'],
356+
'radialGradient': ['cx', 'cy', 'r', 'fx', 'fy'],
357+
'stop': ['offset', 'stop-color', 'stop-opacity'],
358+
}
359+
return bleach.clean(
360+
svg_content,
361+
tags=safe_tags,
362+
attributes=safe_attrs,
363+
strip=True,
364+
)

mobsf/MobSF/settings.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,12 @@
241241
STATIC_URL = '/static/'
242242
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
243243
STATICFILES_STORAGE = 'whitenoise.storage.CompressedStaticFilesStorage'
244-
# 256MB
245-
DATA_UPLOAD_MAX_MEMORY_SIZE = 268435456
244+
# 256MB limit for file uploads
245+
DATA_UPLOAD_MAX_MEMORY_SIZE = 256 * 1024 * 1024
246+
# 400MB per file limit for uncompressed files
247+
ZIP_MAX_UNCOMPRESSED_FILE_SIZE = 400 * 1024 * 1024
248+
# 3GB total limit for all uncompressed files
249+
ZIP_MAX_UNCOMPRESSED_TOTAL_SIZE = 3000 * 1024 * 1024
246250
LOGIN_URL = 'login'
247251
LOGOUT_REDIRECT_URL = '/'
248252
AUTH_PASSWORD_VALIDATORS = [

mobsf/MobSF/views/home.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
python_dict,
3636
)
3737
from mobsf.MobSF.init import api_key
38-
from mobsf.MobSF.security import sanitize_filename
38+
from mobsf.MobSF.security import sanitize_filename, sanitize_svg
3939
from mobsf.MobSF.views.helpers import FileType
4040
from mobsf.MobSF.views.scanning import Scanning
4141
from mobsf.MobSF.views.apk_downloader import apk_download
@@ -402,15 +402,32 @@ def scan_status(request, api=False):
402402

403403
def file_download(dwd_file, filename, content_type):
404404
"""HTTP file download response."""
405-
with open(dwd_file, 'rb') as file:
406-
wrapper = FileWrapper(file)
407-
response = HttpResponse(wrapper, content_type=content_type)
408-
response['Content-Length'] = dwd_file.stat().st_size
405+
def create_response(content, is_binary=True):
406+
"""Helper function to create HTTP response."""
407+
if is_binary:
408+
wrapper = FileWrapper(content)
409+
response = HttpResponse(wrapper, content_type=content_type)
410+
response['Content-Length'] = dwd_file.stat().st_size
411+
else:
412+
response = HttpResponse(content, content_type=content_type)
409413
if filename:
410-
val = f'attachment; filename="{filename}"'
414+
# Remove CRLF from filename to prevent header injection
415+
safe_filename = filename.replace('\r', '').replace('\n', '')
416+
val = f'attachment; filename="{safe_filename}"'
411417
response['Content-Disposition'] = val
412418
return response
413419

420+
# Handle SVG files with bleach cleaning to prevent XSS attacks
421+
if dwd_file.suffix == '.svg':
422+
with open(dwd_file, 'r', encoding='utf-8') as file:
423+
svg_content = file.read()
424+
cleaned_svg = sanitize_svg(svg_content)
425+
return create_response(cleaned_svg, is_binary=False)
426+
427+
# Handle all other binary file types
428+
with open(dwd_file, 'rb') as file:
429+
return create_response(file)
430+
414431

415432
@login_required
416433
@require_http_methods(['GET'])

mobsf/StaticAnalyzer/views/common/shared_func.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ def unzip(checksum, app_path, ext_path):
120120
try:
121121
with zipfile.ZipFile(app_path, 'r') as zipptr:
122122
files = zipptr.namelist()
123+
total_size = 0
124+
stop_fallback_extraction = False
123125
for fileinfo in zipptr.infolist():
124126
ext_path = original_ext_path
125127

@@ -145,8 +147,26 @@ def unzip(checksum, app_path, ext_path):
145147
msg = ('Zip slip detected. skipped extracting'
146148
f' {sanitize_for_logging(file_path)}')
147149
logger.error(msg)
150+
stop_fallback_extraction = True
148151
continue
149152

153+
# Check uncompressed size
154+
if not fileinfo.is_dir():
155+
total_size += fileinfo.file_size
156+
if fileinfo.file_size > settings.ZIP_MAX_UNCOMPRESSED_FILE_SIZE:
157+
size_mb = fileinfo.file_size / (1024 * 1024)
158+
msg = (f'File too large ({size_mb:.2f} MB). Skipping '
159+
f'{sanitize_for_logging(file_path)}')
160+
logger.warning(msg)
161+
if total_size > settings.ZIP_MAX_UNCOMPRESSED_TOTAL_SIZE:
162+
stop_fallback_extraction = True
163+
total_size_mb = total_size / (1024 * 1024)
164+
msg = ('Total uncompressed size '
165+
f'({total_size_mb:.2f} MB) exceeds limit. '
166+
'Aborting extraction.')
167+
logger.error(msg)
168+
raise Exception(msg)
169+
150170
# Fix permissions
151171
if fileinfo.is_dir():
152172
# Directories should have rwxr-xr-x (755)
@@ -167,6 +187,11 @@ def unzip(checksum, app_path, ext_path):
167187
msg = f'Unzipping Error - {str(exp)}'
168188
logger.error(msg)
169189
append_scan_status(checksum, msg, repr(exp))
190+
# Do not fallback to OS unzip
191+
# if the total uncompressed file size is too large
192+
# or if zip slip is detected
193+
if stop_fallback_extraction:
194+
return files
170195
# Fallback to OS unzip
171196
ofiles = os_unzip(checksum, app_path, ext_path)
172197
if not files:

0 commit comments

Comments
 (0)