Three Security Findings in Tautulli: SSRF, JSONP Injection, and SQL Injection

Mar 27, 2026 min read

Background

Tautulli is a Python/CherryPy web application that sits alongside your Plex Media Server and gives you statistics, notifications, and monitoring for everything happening on your server. It is one of the most popular self-hosted Plex companion apps, and a lot of people run it exposed on their home network or even directly on the internet.

I spent some time doing a code review of the Tautulli codebase and found three issues worth writing up. Two of them require no authentication at all, and the third requires admin-level access to exploit. None of these are released fixes yet, but advisories are up and patches have been developed.


Finding 1: Unauthenticated SSRF via pms_image_proxy

What it is

Tautulli has an endpoint called /pms_image_proxy that proxies image requests through the Plex Media Server. The idea is that it fetches artwork and thumbnails from Plex and serves them back to the browser. The problem is that this endpoint has no authentication decorator on it, and it accepts a user-supplied img parameter that can be any URL.

When you pass an HTTP URL to img, Tautulli forwards that URL to your Plex server’s /photo/:/transcode endpoint with the URL as the url parameter. Plex then makes an outbound HTTP request to whatever URL you gave it. You never have to log in to Tautulli to trigger this.

GET /pms_image_proxy?img=http://169.254.169.254/latest/meta-data/ HTTP/1.1
Host: tautulli:8181

Plex picks up the request and reaches out to the target. The response from the target does not come back to you directly (Plex tries to transcode it as an image and fails), so this is a blind SSRF. You can confirm it with an out-of-band callback service.

What it can be used for

Because the request comes from the Plex server, not from Tautulli directly, the attacker is essentially pivoting through Plex. Anything that Plex can reach on the network, the attacker can probe: internal services, cloud metadata endpoints, other hosts on the LAN. In cloud environments where Plex is running on a VM, the AWS/GCP/Azure metadata service at 169.254.169.254 is a classic target.

The SSRF is blind, so you are not reading response bodies directly. But blind SSRF is still useful for network mapping (open vs closed port gives different timing or error behavior) and for hitting unauthenticated internal services that do not care about the response body.

The fix

This one is tricky to fully fix, because the endpoint genuinely needs to make HTTP requests to Plex for legitimate artwork. The fix restricts which URLs are allowed through, rather than blocking external URLs entirely. Worth noting in the advisory: the mitigation narrows the attack surface but does not eliminate the SSRF class entirely, since Plex still has to make outbound requests for metadata.


Finding 2: Unsanitized JSONP Callback Allows Cross-Origin Script Injection

  • GitHub Advisory: GHSA-95mg-wpqw-9qxh
  • CVE: CVE-2026-32275
  • Access required: None for the XSS demo; API key theft only affects instances with no HTTP password set

What it is

The Tautulli API (/api/v2) supports JSONP callbacks via a callback parameter. When you pass callback=myFunction, the API wraps its JSON response in a JavaScript function call and sets the Content-Type to application/javascript. The callback value is taken directly from the request and written into the response with no sanitization or allow-listing.

GET /api/v2?cmd=docs&callback=alert(document.domain)//

Response:

alert(document.domain)//({"response": {"result": "success", ...}});

A few API commands (get_apikey, docs, docs_md) are explicitly allowed to bypass API key authentication. That means a page on any origin can include a <script> tag pointing at your Tautulli instance and the request will succeed without any credentials.

What it can be used for

The most direct impact depends on your Tautulli configuration.

If you have no HTTP password set (which used to be the default and is still common in home setups), the get_apikey command returns your Tautulli API key to unauthenticated callers. Combined with the JSONP issue, a malicious page you visit in your browser can silently steal your API key with a single script tag:

<script src="http://tautulli:8181/api/v2?cmd=get_apikey&callback=stealKey"></script>

Even with a password set, the JSONP issue on its own is still a reflected XSS that executes in the origin of your Tautulli instance. That gives an attacker script execution in the Tautulli browser context if they can get you to load a crafted URL.

The fix

The callback parameter should be validated against a strict allow-list (alphanumeric and dots only, matching valid JavaScript identifiers) before being reflected into the response.


Finding 3: SQL Injection in get_home_stats API

What it is

The get_home_stats API command accepts several filter parameters (section_id, user_id, before, after) that get inserted into SQL queries via Python %-string formatting with no parameterisation. The section_id and user_id fields go in unquoted, so no delimiter escaping is needed at all.

From plexpy/datafactory.py:

where_id += 'AND session_history.section_id = %s ' % section_id
where_id += 'AND session_history.user_id = %s '    % user_id

You can confirm the injection exists even on a fresh install with no watch history, because Python’s sqlite3 module validates SQL syntax at parse time before touching any rows. Send a deliberately broken token and the API returns an error; send valid SQL and it succeeds:

# Syntax error oracle (works on empty databases)
curl "http://tautulli:8181/api/v2?cmd=get_home_stats&apikey=KEY&section_id=0+SQLI_PROOF"
# result: "error"

curl "http://tautulli:8181/api/v2?cmd=get_home_stats&apikey=KEY&section_id=0"
# result: "success"

Once there is any watch history in the database, boolean-blind injection works via a division-by-zero signal: if the injected condition is false, 1/0 is evaluated, SQLite raises an exception, the datafactory catches it and returns None, and the API response flips from "success" to "error".

What it can be used for

With boolean-blind extraction, an attacker who holds the admin API key can read any value out of the Tautulli SQLite database. The most sensitive targets are users.server_token and users.user_token, which are Plex authentication tokens for your Plex friends and managed accounts. Extracting those tokens could let an attacker authenticate to Plex as those users.

The injection is inside a SELECT query’s WHERE clause, and Python’s sqlite3 module blocks multi-statement execution, so write operations are not directly reachable.

A note on severity

This one requires a valid admin API key, which puts it in an awkward spot. If you already have admin access to Tautulli, you can do considerably worse things than SQL injection: the Notification Agent settings let you point Tautulli at an arbitrary shell script and trigger it on any playback event, which is direct OS command execution. So this SQLi does not represent a meaningful privilege escalation for an attacker who already has the key.

The residual risk is that a compromised or colluding admin could silently exfiltrate Plex tokens for other users in a way that does not leave obvious traces in the Tautulli UI. CVSS 3.1 scores this at 4.9 Medium (AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:N/A:N).

The fix

Replace %-string formatting with parameterised queries throughout datafactory.py:

# Before
where_id += 'AND session_history.section_id = %s ' % section_id

# After
where_id += 'AND session_history.section_id = ? '
params.append(section_id)

Timeline

DateEvent
2026-03-02SSRF and JSONP findings submitted to Tautulli
2026-03-03SQL injection finding submitted to Tautulli
2026-03-08Fixes developed by the Tautulli team for all three issues
2026-03-27Vulnerabilities disclosed publicly via GitHub coordinated release

Takeaways

The SSRF and JSONP issues are the more impactful ones here because they require no credentials. If your Tautulli is reachable from the internet (or even from a shared network), the SSRF lets anyone use your Plex server as a network probe, and the JSONP issue lets any web page you visit steal your API key if you haven’t set a login password.

The SQL injection is a good reminder that even admin-only endpoints deserve parameterised queries. The risk is lower when exploitation requires admin access, but the fix is trivial and “it requires an admin key” is not a great reason to leave raw string formatting in database queries.

On the remediation side: it is worth noting that the SSRF cannot be completely fixed without breaking core functionality, since Plex genuinely needs to fetch images over HTTP. That is a real constraint, and the Tautulli team’s approach of restricting the allowed URL patterns rather than blocking the feature entirely is a reasonable call.


All testing was performed on a local lab environment. Findings were responsibly disclosed to the Tautulli team before publication.