CVE-2023-50266: Server-Side Request Forgery (SSRF) (CWE-918)
value: Bazarr manages and downloads subtitles. In version 1.2.4, the proxy method in bazarr/bazarr/app/ui.py does not validate the user-controlled protocol and url variables and passes them to requests.get() without any sanitization, which leads to a blind server-side request forgery (SSRF). This issue allows for crafting GET requests to internal and external resources on behalf of the server. 1.3.1 contains a partial fix, which limits the vulnerability to HTTP/HTTPS protocols.
CWE description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
CVSS v3 base score: 5.3
Published: 2023-12-15T21:15Z
Repository: https://github.com/morpheus65535/bazarr
Language: Python
Files changed: bazarr/app/ui.py
Commit date: 2023-09-18 11:59:45 -0400
Commit message: Fixed some code to prevent arbitrary file read and blind SSRF.

Diff: diff --git a/bazarr/app/ui.py b/bazarr/app/ui.py
index 77708a8c..85d820bd 100644
--- a/bazarr/app/ui.py
+++ b/bazarr/app/ui.py
@@ -141,27 +141,38 @@ def movies_images(url):
 
 
 @check_login
 @ui_bp.route('/system/backup/download/<path:filename>', methods=['GET'])
 def backup_download(filename):
-    return send_file(os.path.join(settings.backup.folder, filename), max_age=0, as_attachment=True)
+    fullpath = os.path.normpath(os.path.join(settings.backup.folder, filename))
+    if not fullpath.startswith(settings.backup.folder):
+        return '', 404
+    else:
+        return send_file(fullpath, max_age=0, as_attachment=True)
 
 
 @ui_bp.route('/api/swaggerui/static/<path:filename>', methods=['GET'])
 def swaggerui_static(filename):
-    return send_file(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'libs', 'flask_restx',
-                     'static', filename))
+    basepath = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'libs', 'flask_restx',
+                            'static')
+    fullpath = os.path.join(basepath, filename)
+    if not fullpath.startswith(basepath):
+        return '', 404
+    else:
+        return send_file(fullpath)
 
 
 def configured():
     System.update({System.configured: '1'}).execute()
 
 
 @check_login
 @ui_bp.route('/test', methods=['GET'])
 @ui_bp.route('/test/<protocol>/<path:url>', methods=['GET'])
 def proxy(protocol, url):
+    if protocol.lower not in ['http', 'https']:
+        return dict(status=False, error='Unsupported protocol')
     url = protocol + '://' + unquote(url)
     params = request.args
     try:
         result = requests.get(url, params, allow_redirects=False, verify=False, timeout=5, headers=headers)
     except Exception as e:

Vulnerable code:     return send_file(os.path.join(settings.backup.folder, filename), max_age=0, as_attachment=True)
    return send_file(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'libs', 'flask_restx',
                     'static', filename))
Fixed code:     fullpath = os.path.normpath(os.path.join(settings.backup.folder, filename))
    if not fullpath.startswith(settings.backup.folder):
        return '', 404
    else:
        return send_file(fullpath, max_age=0, as_attachment=True)
    basepath = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'libs', 'flask_restx',
                            'static')
    fullpath = os.path.join(basepath, filename)
    if not fullpath.startswith(basepath):
        return '', 404
    else:
        return send_file(fullpath)
    if protocol.lower not in ['http', 'https']:
        return dict(status=False, error='Unsupported protocol')