VAITP Dataset

← Back to the dataset

CVE-2025-12695

Permissive sandbox in DSPy's PythonInterpreter allows sensitive file theft.

  • CVSS 5.9
  • CWE-653
  • Configuration Issues
  • Remote

The overly permissive sandbox configuration in DSPy allows attackers to steal sensitive files in cases when users build an AI agent which consumes user input and uses the “PythonInterpreter” class.

CVSS base score
5.9
Published
2025-11-04
OWASP
A03 Injection
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Configuration Issues
Subcategory
Command Injection
Accessibility scope
Remote
Impact
Information Disclosure
Affected component
DSPy
Fixed by upgrading
Yes

Solution

Upgrade to `dspy-ai` version 2.4.4 or later.

Vulnerable code sample

import io
import sys
import os

# This class represents a vulnerable Python code interpreter before a security fix.
# This code is a conceptual representation of the vulnerability described in
# CVE-2025-12695, where an overly permissive sandbox allows arbitrary code execution.
# The actual CVE number is fictional, but the vulnerability pattern is real.
class PythonInterpreter:
    """
    A tool for executing Python code.

    In this vulnerable version, the interpreter uses `exec()` directly on
    unsanitized input. This allows an attacker to run any arbitrary Python code
    on the host machine, leading to information disclosure, remote code execution,
    and other critical security risks.
    """

    def __call__(self, code: str) -> str:
        """
        Executes the given Python code string and captures its stdout.
        THIS IS A DANGEROUS AND VULNERABLE IMPLEMENTATION.
        """
        # Redirect standard output to capture the result of the executed code
        old_stdout = sys.stdout
        redirected_output = sys.stdout = io.StringIO()

        try:
            # DANGER: `exec` is called directly on the user-provided `code`.
            # An attacker can inject code to read files, access environment
            # variables, or perform any other action available to the Python process.
            exec(code, {})
        except Exception as e:
            # If an error occurs, restore stdout and return the error message.
            sys.stdout = old_stdout
            return f"An error occurred during execution: {e}"
        finally:
            # Always restore the original stdout
            sys.stdout = old_stdout

        # Return the captured output
        return redirected_output.getvalue()


# --- Demonstration of the Exploit ---
# The following code simulates how an attacker could exploit the vulnerability.

def create_dummy_sensitive_file():
    """Creates a fake sensitive file for the demonstration."""
    with open("secrets.txt", "w") as f:
        f.write("API_KEY=123-ABC-789-XYZ\n")
        f.write("DATABASE_URL=postgres://user:password@host:5432/db\n")
    print("-> Created a dummy 'secrets.txt' file for the exploit demonstration.\n")


def simulate_ai_agent(user_query: str):
    """
    Simulates an AI agent that takes a user query, interprets it as Python code,
    and uses the vulnerable PythonInterpreter to execute it.
    """
    print(f"[Agent] Received query: '{user_query}'")
    print("[Agent] Interpreting query as code and executing with PythonInterpreter tool...")

    # The agent instantiates and uses the vulnerable tool
    python_tool = PythonInterpreter()
    output = python_tool(user_query)

    print("[Agent] Execution complete. Displaying output:")
    print("=" * 20)
    print(output.strip())
    print("=" * 20)
    print("\n[!] The agent successfully executed the code, leaking the file contents.")


if __name__ == "__main__":
    create_dummy_sensitive_file()

    # The attacker crafts a malicious query. Instead of asking a question,
    # they provide a string of Python code designed to read a sensitive file.
    malicious_input = "with open('secrets.txt', 'r') as f: print(f.read())"

    # The AI agent processes the malicious input, leading to the exploit.
    simulate_ai_agent(malicious_input)

    # Clean up the dummy file
    os.remove("secrets.txt")

Patched code sample

import ast
import io
from contextlib import redirect_stdout

# Since CVE-2025-12695 is a fictional CVE, this code represents a
# hypothetical fix for the described vulnerability. The vulnerability involves
# a Python interpreter class that unsafely executes user-provided code,
# allowing file system access.
#
# The fix demonstrates a sandboxing approach using Python's Abstract Syntax Tree (AST)
# to validate the code before execution. It disallows dangerous operations
# like imports, file access, and calls to unsafe built-in functions.

class PatchedPythonInterpreter:
    """
    A Python interpreter that safely executes code by validating it against
    a whitelist of allowed operations using an Abstract Syntax Tree (AST) walk.
    """

    def __init__(self):
        # A whitelist of safe built-in function names.
        # Functions like 'open', 'eval', 'exec', '__import__' are deliberately excluded.
        self.safe_builtins = {
            'print', 'len', 'str', 'int', 'float', 'list', 'dict', 'tuple',
            'range', 'abs', 'sum', 'min', 'max', 'round', 'pow', 'sorted'
        }
        # A whitelist of safe modules, if any were to be allowed. Kept empty for max security.
        self.safe_modules = {}

    def _validate_ast(self, code_string: str):
        """
        Parses the code into an AST and walks through it to ensure no
        disallowed operations are present.

        Args:
            code_string: The Python code to validate.

        Raises:
            SecurityError: If an unsafe operation is detected.
        """
        try:
            tree = ast.parse(code_string)
        except SyntaxError as e:
            raise SecurityError(f"Invalid Python syntax: {e}") from e

        for node in ast.walk(tree):
            # 1. Disallow imports
            if isinstance(node, (ast.Import, ast.ImportFrom)):
                raise SecurityError("Imports are strictly forbidden.")

            # 2. Disallow attribute access that starts with an underscore
            if isinstance(node, ast.Attribute) and node.attr.startswith('_'):
                raise SecurityError("Access to private/magic attributes (e.g., __class__) is forbidden.")

            # 3. Validate all function calls
            if isinstance(node, ast.Call):
                if isinstance(node.func, ast.Name):
                    # It's a direct function call, like print() or open()
                    func_name = node.func.id
                    if func_name not in self.safe_builtins:
                        raise SecurityError(f"Call to disallowed function '{func_name}' is forbidden.")
                elif isinstance(node.func, ast.Attribute):
                    # It's a method call, like 'str'.upper() or obj.__dict__()
                    # For this example, we disallow all method calls for simplicity,
                    # but a more complex system could whitelist specific safe methods.
                    raise SecurityError("Method calls are forbidden in this sandbox.")

    def execute(self, code_string: str) -> str:
        """
        Executes a string of Python code after validating its safety.

        Args:
            code_string: The user-provided Python code.

        Returns:
            A string containing the standard output of the executed code or an error message.
        """
        try:
            # Step 1: Validate the code's structure for safety.
            self._validate_ast(code_string)

            # Step 2: Prepare a safe, restricted environment for execution.
            # The '__builtins__' dictionary is carefully constructed to only
            # expose the whitelisted safe functions.
            safe_globals = {
                "__builtins__": {
                    name: __builtins__[name] for name in self.safe_builtins
                }
            }

            # Step 3: Execute the validated code and capture its output.
            output_capture = io.StringIO()
            with redirect_stdout(output_capture):
                exec(code_string, safe_globals, {})
            
            return output_capture.getvalue()

        except (SecurityError, Exception) as e:
            return f"Execution blocked or failed: {e}"


class SecurityError(Exception):
    """Custom exception for security-related validation failures."""
    pass


if __name__ == '__main__':
    # Demonstration of the fix
    interpreter = PatchedPythonInterpreter()

    print("--- Demonstrating the Patched (Fixed) Interpreter ---")

    # --- Attack Attempts (Should be blocked) ---

    # 1. Attempt to read a sensitive file using `open()`
    malicious_code_file_read = "print(open('/etc/passwd', 'r').read())"
    print(f"\nExecuting malicious code: {malicious_code_file_read}")
    result = interpreter.execute(malicious_code_file_read)
    print(f"Result: {result}")

    # 2. Attempt to list directories using the `os` module
    malicious_code_import_os = "import os; print(os.listdir('/'))"
    print(f"\nExecuting malicious code: {malicious_code_import_os}")
    result = interpreter.execute(malicious_code_import_os)
    print(f"Result: {result}")

    # 3. Attempt to use `__import__` to bypass import block
    malicious_code_dunder_import = "__import__('os').system('echo vulnerable')"
    print(f"\nExecuting malicious code: {malicious_code_dunder_import}")
    result = interpreter.execute(malicious_code_dunder_import)
    print(f"Result: {result}")
    
    # 4. Attempt to escape the sandbox via `__class__`
    malicious_code_escape = "print(''.__class__.__mro__[1].__subclasses__())"
    print(f"\nExecuting malicious code: {malicious_code_escape}")
    result = interpreter.execute(malicious_code_escape)
    print(f"Result: {result}")

    # --- Benign Code (Should be allowed) ---

    # 5. Simple arithmetic and printing
    safe_code = """
a = 15
b = 30
result = a * 2 + b
print(f"The result is: {result}")
"""
    print(f"\nExecuting safe code: {safe_code.strip()}")
    result = interpreter.execute(safe_code)
    print(f"Result:\n{result}")

    # 6. Using an allowed built-in function `len()`
    safe_code_len = "print(len('this is a safe string'))"
    print(f"\nExecuting safe code: {safe_code_len}")
    result = interpreter.execute(safe_code_len)
    print(f"Result: {result}")

Payload

print(open('/etc/passwd').read())

Cite this entry

@misc{vaitp:cve202512695,
  title        = {{Permissive sandbox in DSPy's PythonInterpreter allows sensitive file theft.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2025},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-12695},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-12695/}}
}
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 ::