VAITP Dataset

← Back to the dataset

CVE-2025-58761

Unauthenticated path traversal in Tautulli allows arbitrary file read.

  • CVSS 7.5
  • CWE-27
  • Input Validation and Sanitization
  • Remote

Tautulli is a Python based monitoring and tracking tool for Plex Media Server. The `real_pms_image_proxy` endpoint in Tautulli v2.15.3 and prior is vulnerable to path traversal, allowing unauthenticated attackers to read arbitrary files from the application server's filesystem. The `real_pms_image_proxy` is used to fetch an image directly from the backing Plex Media Server. The image to be fetched is specified through an `img` URL parameter, which can either be a URL or a file path. There is some validation ensuring that `img` begins with the prefix `interfaces/default/images` in order to be served from the local filesystem. However this can be bypassed by passing an `img` parameter which begins with a valid prefix, and then adjoining path traversal characters in order to reach files outside of intended directories. An attacker can exfiltrate files on the application file system, including the `tautulli.db` SQLite database containing active JWT tokens, as well as the `config.ini` file which contains the hashed admin password, the JWT token secret, and the Plex Media Server token and connection details. If the password is cracked, or if a valid JWT token is present in the database, an unauthenticated attacker can escalate their privileges to obtain administrative control over the application. Version 2.16.0 contains a fix for the issue.

CVSS base score
7.5
Published
2025-09-09
OWASP
A01 Broken Access Control
Orthogonal defect classification
Checking
Code defect classification
Incorrect Check
Category
Input Validation and Sanitization
Subcategory
Path Traversal
Accessibility scope
Remote
Impact
Information Disclosure
Affected component
Tautulli

Solution

Upgrade Tautulli to version 2.16.0 or later.

Vulnerable code sample

import os
from flask import Flask, request, abort, send_from_directory

# This is a simplified, representative example of the vulnerability
# described in CVE-2025-58761 for Tautulli v2.15.3 and prior.
# This code is for educational and demonstration purposes only.
#
# To test this vulnerable code:
# 1. Install Flask: `pip install Flask`
# 2. Create the following directory structure and files:
#    mkdir -p data/interfaces/default/images
#    echo "pms_token = FAKE_PLEX_TOKEN" > data/config.ini
#    echo "This is a safe image" > data/interfaces/default/images/safe_image.png
# 3. Run this Python script.
# 4. Use curl to exploit the vulnerability:
#    curl "http://127.0.0.1:5000/api/v2?cmd=real_pms_image_proxy&img=interfaces/default/images/../../../config.ini"

app = Flask(__name__)

# Assume the application's root data directory is a 'data' folder.
DATA_DIR = os.path.abspath('data')


@app.route('/api/v2')
def api_endpoint():
    """
    Simulates the Tautulli API endpoint that dispatches commands.
    """
    if request.args.get('cmd') == 'real_pms_image_proxy':
        return real_pms_image_proxy()
    abort(404)


def real_pms_image_proxy():
    """
    This function contains the logic vulnerable to path traversal.
    """
    img_param = request.args.get('img')

    if not img_param:
        abort(400, "Missing 'img' parameter")

    # THE VULNERABLE CHECK:
    # The code validates that the path *starts with* an allowed prefix.
    # However, it does not sanitize or block path traversal characters ('../').
    # An attacker can pass 'interfaces/default/images/../../../config.ini',
    # which satisfies this condition.
    if img_param.startswith('interfaces/default/images'):

        # The unsanitized user input is joined to the base data directory.
        # os.path.join will process the '..' characters, allowing the path
        # to "escape" the intended 'images' directory.
        file_path = os.path.join(DATA_DIR, img_param)

        # The path is then normalized, resolving sequences like '..' and creating
        # a final, absolute path to a file outside the intended directory.
        # For example, on Linux:
        # /app/data/interfaces/default/images/../../../config.ini
        # becomes:
        # /app/data/config.ini
        normalized_path = os.path.normpath(file_path)

        # The application proceeds to serve the file from the traversed path,
        # leading to arbitrary file read. A secure implementation would verify
        # that the 'normalized_path' is still within the allowed base directory.
        try:
            if not os.path.isfile(normalized_path):
                abort(404)

            directory = os.path.dirname(normalized_path)
            filename = os.path.basename(normalized_path)
            
            # The file at the traversed path is sent to the user.
            return send_from_directory(directory, filename)
        except (FileNotFoundError, ValueError):
            abort(404)

    # Code for handling remote URLs or other non-vulnerable cases.
    else:
        abort(403, "Path is not in the allowed directory.")


if __name__ == '__main__':
    if not os.path.exists('data/config.ini'):
        print("Error: Please create the dummy file structure as described in the code comments.")
    else:
        print("Vulnerable server is running on http://127.0.0.1:5000")
        print("Try to access the sensitive config.ini file with:")
        print("curl \"http://127.0.0.1:5000/api/v2?cmd=real_pms_image_proxy&img=interfaces/default/images/../../../config.ini\"")
        app.run(debug=False, port=5000)

Patched code sample

import os

def get_secure_filepath(user_path, base_dir):
    """Secure function that fixes the vulnerability."""
    # SECURE: This version prevents path traversal
    """
    This function represents the fix for the path traversal vulnerability.
    It validates that the user-provided path resolves to a location
    within an allowed directory.
    """
    # Define the directory where files are allowed to be accessed from.
    # This acts as a security "jail".
    allowed_dir = os.path.realpath(os.path.join(base_dir, 'interfaces', 'default', 'images'))

    # The original vulnerable code only checked for this prefix. We keep it
    # as a preliminary check.
    if not user_path.startswith('interfaces/default/images'):
        return None

    # Construct the full path from user input and resolve it to its
    # canonical, absolute path. os.path.realpath will collapse any '..'
    # traversal elements and resolve symbolic links.
    # For example:
    # '/app/data/interfaces/default/images/../../../config.ini'
    # becomes '/app/data/config.ini'.
    requested_path = os.path.realpath(os.path.join(base_dir, user_path))

    # The core of the fix:
    # We use os.path.commonpath to check if the resolved path of the
    # requested file is located within the allowed directory. If the common
    # path of the two is not the allowed directory itself, it means the
    # requested_path has "escaped" the jail.
    if os.path.commonpath([requested_path, allowed_dir]) != allowed_dir:
        # Path traversal attempt detected, as the path is outside the allowed directory.
        return None

    # An additional check to ensure the path is a file and not a directory.
    if not os.path.isfile(requested_path):
        return None

    return requested_path

Payload

/pms_image_proxy?img=interfaces/default/images/../../../config.ini

Cite this entry

@misc{vaitp:cve202558761,
  title        = {{Unauthenticated path traversal in Tautulli allows arbitrary file read.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2025},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-58761},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-58761/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::