VAITP Dataset

← Back to the dataset

CVE-2026-28505

Tautulli sandbox escape in notification templates using lambda expressions.

  • CVSS 7.5
  • CWE-94
  • Design Defects
  • Remote

Tautulli is a Python based monitoring and tracking tool for Plex Media Server. Prior to version 2.17.0, the str_eval() function in notification_handler.py implements a sandboxed eval() for notification text templates. The sandbox attempts to restrict callable names by inspecting code.co_names of the compiled code object. However, co_names only contains names from the outer code object. When a lambda expression is used, it creates a nested code object whose attribute accesses are stored in code.co_consts, NOT in code.co_names. The sandbox never inspects nested code objects. This issue has been patched in version 2.17.0.

CVSS base score
7.5
Published
2026-03-30
OWASP
A08 Software and Data Integrity Failures
Orthogonal defect classification
Checking
Code defect classification
Incorrect Check
Category
Design Defects
Subcategory
Command Injection
Accessibility scope
Remote
Impact
Arbitrary Code Execution
Affected component
Tautulli
Fixed by upgrading
Yes

Solution

Upgrade Tautulli to version 2.17.0 or later.

Vulnerable code sample

import sys
import datetime

# This code represents a vulnerable implementation similar to the one
# described in CVE-2026-28505 in Tautulli's notification_handler.py before version 2.17.0.

BANNED_NAMES = (
    '__import__',
    'eval',
    'exec',
    'open',
    'os',
    'system',
    'subprocess',
    'sys',
    'breakpoint',
    'compile',
    'globals',
    'locals'
)

# A simplified set of globals that might be allowed in the template evaluation
ALLOWED_GLOBALS = {
    '__builtins__': {
        'abs': abs,
        'all': all,
        'any': any,
        'bool': bool,
        'str': str,
        'int': int,
        'float': float,
        'len': len,
        'list': list,
        'dict': dict,
        'set': set,
        'tuple': tuple,
        'max': max,
        'min': min,
        'round': round,
        'sum': sum,
    },
    'datetime': datetime,
}


def str_eval(text, eval_globals=None, eval_locals=None):
    """
    A sandboxed eval function for evaluating template strings.
    This version is vulnerable because it only checks the co_names of the top-level
    code object and does not recursively check nested code objects (e.g., from lambdas).
    """
    _globals = ALLOWED_GLOBALS.copy()
    if eval_globals:
        _globals.update(eval_globals)

    if eval_locals is None:
        eval_locals = {}

    try:
        # Compile the expression to inspect it before evaluation.
        code = compile(text, '<string>', 'eval')

        # VULNERABILITY: This check is insufficient.
        # It only inspects the names in the outer code object's namespace.
        # It does not inspect nested code objects, such as those created by a lambda.
        for name in code.co_names:
            if name in BANNED_NAMES:
                raise NameError(f"Use of '{name}' is not allowed.")
            # A real implementation would have more checks here.

        # If the checks pass, the code is evaluated.
        # A malicious lambda will pass the check above and be executed here.
        return eval(code, _globals, eval_locals)

    except Exception as e:
        # In a real application, this would likely log the error.
        return f"Error: {e}"

# Example of how the vulnerability would be exploited.
# This part is for demonstration and would not be part of the original file.
#
# Malicious payload that bypasses the sandbox check.
# The `co_names` for this entire expression is empty `()`.
# The `__import__`, `os`, and `system` names are stored in the nested code
# object of the lambda, which is never inspected by the vulnerable function.
#
# evil_string = "(lambda: __import__('os').system('echo VULNERABLE'))()"
#
# # The function fails to detect the banned names and executes the payload.
# result = str_eval(evil_string)
#
# # This would print "VULNERABLE" to the console, and `result` would be the
# # exit code of the command (e.g., 0).

Patched code sample

def str_eval(s, safe_names=None, user_safe_names=None):
    # A very simple sandboxed eval
    if not s:
        return ""

    safe_names = safe_names or {}
    user_safe_names = user_safe_names or {}
    safe_names.update(user_safe_names)

    if not isinstance(s, six.string_types):
        logger.debug(u"Tautulli Notifiers :: Unsafe eval, not a string: '{string}'".format(string=s))
        return s

    def _is_safe(code_obj, safe_names):
        """Recursively check for unsafe names in code objects."""
        for name in code_obj.co_names:
            if name not in safe_names:
                return False
        for const in code_obj.co_consts:
            if isinstance(const, type(code_obj)):
                if not _is_safe(const, safe_names):
                    return False
        return True

    try:
        code = compile(s, '<string>', 'eval')

        if not _is_safe(code, safe_names):
            logger.warning(
                u"Tautulli Notifiers :: Unsafe eval characters stripped from string: "
                u"'{string}'".format(string=s))
            return s

        return eval(code, {'__builtins__': {}}, safe_names)

    except (SyntaxError, NameError, TypeError, ZeroDivisionError) as e:
        logger.warning(u"Tautulli Notifiers :: Unsafe eval: '{string}', {error}".format(string=s, error=e))
        return s

Payload

`(lambda: __import__('os').system('id'))()`

Cite this entry

@misc{vaitp:cve202628505,
  title        = {{Tautulli sandbox escape in notification templates using lambda expressions.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-28505},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-28505/}}
}
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 ::