CVE-2023-50265: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: Bazarr manages and downloads subtitles. Prior to 1.3.1, the /api/swaggerui/static endpoint in bazarr/app/ui.py does not validate the user-controlled filename variable and uses it in the send_file function, which leads to an arbitrary file read on the system. This issue is fixed in version 1.3.1.
CWE description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
CVSS v3 base score: 7.5
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')