CVE-2026-43986
Tautulli < 2.17.1 has an unauthenticated SSRF via its image proxy.
- CVSS 9.9
- CWE-918
- Design Defects
- Remote
Tautulli is a Python based monitoring and tracking tool for Plex Media Server. Versions prior to 2.17.1 expose a public `/image/<hash>` route that resolves attacker-controlled entries from `image_hash_lookup` and replays them through the same server-side image fetch logic used by authenticated image proxying. A low-privilege guest user can seed a malicious external image URL into this lookup table and then trigger server-side fetches through a fully unauthenticated endpoint. This turns an authenticated SSRF primitive into a persistent unauthenticated SSRF gadget. Once the malicious hash entry exists, any external user can request `/image/<hash>.png` and cause the PMS or Tautulli host to fetch an arbitrary attacker-chosen URL. Version 2.17.1 patches the issue.
- CWE
- CWE-918
- CVSS base score
- 9.9
- Published
- 2026-06-04
- OWASP
- A10 Server-Side Request Forgery
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Design Defects
- Subcategory
- Server-Side Request Forgery (SSRF)
- Accessibility scope
- Remote
- Impact
- Information Disclosure
- Affected component
- Tautulli
- Fixed by upgrading
- Yes
Solution
Upgrade Tautulli to version 2.17.1 or later.
Vulnerable code sample
import hashlib
import requests
from flask import Flask, request, Response
app = Flask(__name__)
# This in-memory dictionary simulates the 'image_hash_lookup' table
# that persists attacker-controlled URLs.
image_hash_lookup = {}
# This endpoint represents a function that an authenticated, low-privilege
# user can access. It allows them to 'seed' the lookup table with a URL
# of their choice, which generates a corresponding hash.
@app.route('/authenticated/seed_image_url', methods=['POST'])
def seed_image_url():
# In a real application, this endpoint would have authentication checks.
image_url = request.json.get('url')
if not image_url:
return "URL is required", 400
# Create a predictable hash from the URL for the attacker to use.
url_hash = hashlib.md5(image_url.encode('utf-8')).hexdigest()
# The attacker's URL is stored, creating a persistent entry.
image_hash_lookup[url_hash] = image_url
return {"status": "seeded", "hash": url_hash}, 200
# VULNERABLE PUBLIC ENDPOINT (pre-patch)
# This route is unauthenticated and publicly accessible. It resolves a hash
# from the lookup table and fetches the associated URL.
@app.route('/image/<image_hash>')
def get_image_by_hash(image_hash):
# The server retrieves a URL using the user-provided hash.
# The URL itself was previously stored by an attacker.
target_url = image_hash_lookup.get(image_hash)
if not target_url:
return "Image hash not found", 404
try:
# THE VULNERABILITY: Server-Side Request Forgery (SSRF)
# The server makes a GET request to the attacker-controlled URL.
# This can be used to scan internal networks, access cloud metadata
# services, or interact with internal-only web applications.
ssrf_response = requests.get(target_url, timeout=5)
# The content of the fetched URL is then returned to the user.
return Response(ssrf_response.content,
content_type=ssrf_response.headers.get('Content-Type'))
except requests.exceptions.RequestException as e:
return f"Failed to fetch resource: {e}", 502Patched code sample
import functools
from flask import Flask, request, abort
# This example uses the Flask framework to simulate Tautulli's web server.
app = Flask(__name__)
# A mock database representing the 'image_hash_lookup' table.
# A low-privilege user has already "seeded" a malicious URL.
IMAGE_HASH_LOOKUP = {
'd34db33f': 'http://legitimate.image.host/image.png',
# Malicious entry for SSRF, pointing to internal cloud metadata service
'a1b2c3d4': 'http://169.254.169.254/latest/meta-data/'
}
# --- The Fix ---
# The vulnerability was that the /image/<hash> endpoint was unauthenticated.
# The fix is to enforce authentication on this endpoint, preventing an
# unauthenticated attacker from triggering the server-side request.
# This is represented by the `login_required` decorator.
def login_required(f):
"""A simple decorator to simulate checking for user authentication."""
@functools.wraps(f)
def decorated_function(*args, **kwargs):
# In a real application, this would inspect a session cookie or auth header.
# For this example, we'll just check for a 'token' in the request headers.
# If the token is missing, it returns a 401 Unauthorized error.
if 'token' not in request.headers:
abort(401, description="Authentication required.")
# If authenticated, proceed to the original function.
return f(*args, **kwargs)
return decorated_function
# The vulnerable endpoint was public. The fixed endpoint is now protected
# by the `login_required` decorator.
@app.route('/image/<image_hash>')
@login_required # <-- THIS DECORATOR IS THE FIX
def get_image_by_hash(image_hash):
"""
Fetches an image using a hash. Access is now restricted to authenticated
users, preventing the unauthenticated SSRF vulnerability.
"""
image_url = IMAGE_HASH_LOOKUP.get(image_hash)
if not image_url:
abort(404, description="Image hash not found.")
# The server-side request would happen here.
# By requiring authentication first, we prevent an unauthenticated
# attacker from triggering this fetch logic. The SSRF primitive is
# now only accessible to authenticated users, matching the behavior
# of other authenticated image proxying features.
print(f"Authenticated user triggered fetch for URL: {image_url}")
# In a real app, the server would fetch the URL and return the image data.
return f"Authenticated fetch for {image_url} would now proceed.", 200Payload
http://169.254.169.254/latest/meta-data/iam/security-credentials/
Cite this entry
@misc{vaitp:cve202643986,
title = {{Tautulli < 2.17.1 has an unauthenticated SSRF via its image proxy.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-43986},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-43986/}}
}
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 ::
