VYPR
Critical severityNVD Advisory· Published Jul 27, 2025· Updated Jul 28, 2025

Sandbox Escape Vulnerability in huggingface/smolagents

CVE-2025-5120

Description

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.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A sandbox escape in smolagents 1.14.0 allows attackers to bypass restrictions in local_python_executor.py and achieve remote code execution.

Vulnerability

Overview

CVE-2025-5120 is a sandbox escape vulnerability identified in the local_python_executor.py module of the huggingface/smolagents library, version 1.14.0. The module implements static and dynamic checks intended to restrict Python code execution to a set of whitelisted modules and functions, but these checks are inadequate. Attackers can craft inputs that bypass the restrictions, allowing arbitrary Python code to be executed outside the sandboxed environment. This undermines the core security boundary that isolates untrusted code from the host system [2].

Exploitation

To exploit this vulnerability, an attacker needs to provide malicious input to a CodeAgent (the library's agent that writes and executes code). The restricted environment relies on a whitelist of allowed imports and functions, but deficiencies in the check_safer_result and safer_eval mechanisms — specifically related to submodules accessed through indirect attribute access — enable circumvention of these controls. The official commit addressing the issue (33a942e) shows additional checks added to prevent access to forbidden submodules and dangerous functions, confirming the nature of the bypass [3]. No authentication is required past the ability to supply code to the agent, making the attack surface broad in scenarios where agents process untrusted input.

Impact

Successful exploitation grants the attacker remote code execution (RCE) on the host system. This can lead to unauthorized code execution, data leakage, and compromise of the entire integration environment. The vulnerability has a critical CVSS score, emphasizing the severity of the security boundary breach [2]. As smolagents is designed to run agents that can execute code, this flaw negates the protection mechanisms intended to safeguard user environments.

Mitigation

The issue is fully resolved in version 1.17.0 of smolagents. Users are strongly advised to upgrade immediately. No workaround is provided; the fix involves enhanced validation of return values and stricter checks on module and function access [1][3][4]. No evidence of exploitation in the wild has been reported as of the publication date.

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
smolagentsPyPI
< 1.17.01.17.0

Affected products

2

Patches

1
33a942e62b6f

Prevent submodules through indirect attribute access in LocalPythonExecutor (#1375)

https://github.com/huggingface/smolagentsAlbert Villanova del MoralMay 26, 2025via ghsa
2 files changed · +140 44
  • src/smolagents/local_python_executor.py+82 41 modified
    @@ -146,6 +146,87 @@ def nodunder_getattr(obj, name, default=None):
     ]
     
     
    +def check_safer_result(result: Any, static_tools: dict[str, Callable] = None, authorized_imports: list[str] = None):
    +    """
    +    Checks if a result is safer according to authorized imports and static tools.
    +
    +    Args:
    +        result (Any): The result to check.
    +        static_tools (dict[str, Callable]): Dictionary of static tools.
    +        authorized_imports (list[str]): List of authorized imports.
    +
    +    Raises:
    +        InterpreterError: If the result is not safe
    +    """
    +    if isinstance(result, ModuleType):
    +        if not check_import_authorized(result.__name__, authorized_imports):
    +            raise InterpreterError(f"Forbidden access to module: {result.__name__}")
    +    elif isinstance(result, dict) and result.get("__spec__"):
    +        if not check_import_authorized(result["__name__"], authorized_imports):
    +            raise InterpreterError(f"Forbidden access to module: {result['__name__']}")
    +    elif isinstance(result, (FunctionType, BuiltinFunctionType)):
    +        for qualified_function_name in DANGEROUS_FUNCTIONS:
    +            module_name, function_name = qualified_function_name.rsplit(".", 1)
    +            if (
    +                (static_tools is None or function_name not in static_tools)
    +                and result.__name__ == function_name
    +                and result.__module__ == module_name
    +            ):
    +                raise InterpreterError(f"Forbidden access to function: {function_name}")
    +
    +
    +def safer_eval(func: Callable):
    +    """
    +    Decorator to enhance the security of an evaluation function by checking its return value.
    +
    +    Args:
    +        func (Callable): Evaluation function to be made safer.
    +
    +    Returns:
    +        Callable: Safer evaluation function with return value check.
    +    """
    +
    +    @wraps(func)
    +    def _check_return(
    +        expression,
    +        state,
    +        static_tools,
    +        custom_tools,
    +        authorized_imports=BASE_BUILTIN_MODULES,
    +    ):
    +        result = func(expression, state, static_tools, custom_tools, authorized_imports=authorized_imports)
    +        check_safer_result(result, static_tools, authorized_imports)
    +        return result
    +
    +    return _check_return
    +
    +
    +def safer_func(
    +    func: Callable,
    +    static_tools: dict[str, Callable] = BASE_PYTHON_TOOLS,
    +    authorized_imports: list[str] = BASE_BUILTIN_MODULES,
    +):
    +    """
    +    Decorator to enhance the security of a function call by checking its return value.
    +
    +    Args:
    +        func (Callable): Function to be made safer.
    +        static_tools (dict[str, Callable]): Dictionary of static tools.
    +        authorized_imports (list[str]): List of authorized imports.
    +
    +    Returns:
    +        Callable: Safer function with return value check.
    +    """
    +
    +    @wraps(func)
    +    def _check_return(*args, **kwargs):
    +        result = func(*args, **kwargs)
    +        check_safer_result(result, static_tools, authorized_imports)
    +        return result
    +
    +    return _check_return
    +
    +
     class PrintContainer:
         def __init__(self):
             self.value = ""
    @@ -245,46 +326,6 @@ def check_import_authorized(import_to_check: str, authorized_imports: list[str])
         return True
     
     
    -def safer_eval(func: Callable):
    -    """
    -    Decorator to make the evaluation of a function safer by checking its return value.
    -
    -    Args:
    -        func: Function to make safer.
    -
    -    Returns:
    -        Callable: Safer function with return value check.
    -    """
    -
    -    @wraps(func)
    -    def _check_return(
    -        expression,
    -        state,
    -        static_tools,
    -        custom_tools,
    -        authorized_imports=BASE_BUILTIN_MODULES,
    -    ):
    -        result = func(expression, state, static_tools, custom_tools, authorized_imports=authorized_imports)
    -        if isinstance(result, ModuleType):
    -            if not check_import_authorized(result.__name__, authorized_imports):
    -                raise InterpreterError(f"Forbidden access to module: {result.__name__}")
    -        elif isinstance(result, dict) and result.get("__spec__"):
    -            if not check_import_authorized(result["__name__"], authorized_imports):
    -                raise InterpreterError(f"Forbidden access to module: {result['__name__']}")
    -        elif isinstance(result, (FunctionType, BuiltinFunctionType)):
    -            for qualified_function_name in DANGEROUS_FUNCTIONS:
    -                module_name, function_name = qualified_function_name.rsplit(".", 1)
    -                if (
    -                    function_name not in static_tools
    -                    and result.__name__ == function_name
    -                    and result.__module__ == module_name
    -                ):
    -                    raise InterpreterError(f"Forbidden access to function: {function_name}")
    -        return result
    -
    -    return _check_return
    -
    -
     def evaluate_attribute(
         expression: ast.Attribute,
         state: dict[str, Any],
    @@ -824,7 +865,7 @@ def evaluate_name(
         if name.id in state:
             return state[name.id]
         elif name.id in static_tools:
    -        return static_tools[name.id]
    +        return safer_func(static_tools[name.id], static_tools=static_tools, authorized_imports=authorized_imports)
         elif name.id in custom_tools:
             return custom_tools[name.id]
         elif name.id in ERRORS:
    
  • tests/test_local_python_executor.py+58 3 modified
    @@ -761,7 +761,8 @@ def test_types_as_objects(self):
             code = "type_a = float(2); type_b = str; type_c = int"
             state = {}
             result, is_final_answer = evaluate_python_code(code, {"float": float, "str": str, "int": int}, state=state)
    -        assert result is int
    +        # Result is wrapped by safer_func
    +        assert result.__wrapped__ is int
     
         def test_tuple_id(self):
             code = """
    @@ -1133,7 +1134,9 @@ class TestClass:
     
             assert result == (5, "test")
             assert isinstance(state["TestClass"], type)
    -        assert state["TestClass"].__annotations__ == {"x": int, "y": str}
    +        # Values are wrapped by safer_func
    +        annotations = {key: value.__wrapped__ for key, value in state["TestClass"].__annotations__.items()}
    +        assert annotations == {"x": int, "y": str}
             assert state["TestClass"].x == 5
             assert state["TestClass"].y == "test"
             assert isinstance(state["instance"], state["TestClass"])
    @@ -1187,7 +1190,9 @@ class TestClass:
     
             assert result == ("value", ["b", 30])
             assert isinstance(state["TestClass"], type)
    -        assert state["TestClass"].__annotations__ == {"key_data": dict, "index_data": list}
    +        # Values are wrapped by safer_func
    +        annotations = {key: value.__wrapped__ for key, value in state["TestClass"].__annotations__.items()}
    +        assert annotations == {"key_data": dict, "index_data": list}
             assert state["TestClass"].key_data == {"key": "value"}
             assert state["TestClass"].index_data == ["a", "b", 30]
     
    @@ -2075,6 +2080,7 @@ def test_vulnerability_via_importlib(self, additional_authorized_imports, expect
                     ["threading"],
                     InterpreterError("Forbidden access to module: sys"),
                 ),
    +            ("import warnings; warnings.sys", ["warnings"], InterpreterError("Forbidden access to module: sys")),
                 # Allowed
                 ("import pandas; pandas.io", ["pandas", "pandas.io"], None),
             ],
    @@ -2088,6 +2094,55 @@ def test_vulnerability_via_submodules(self, code, additional_authorized_imports,
             ):
                 executor(code)
     
    +    @pytest.mark.parametrize(
    +        "code, additional_authorized_imports, expected_error",
    +        [
    +            # Using filter with functools.partial
    +            (
    +                dedent(
    +                    """
    +                    import functools
    +                    import warnings
    +                    list(filter(functools.partial(getattr, warnings), ["sys"]))
    +                    """
    +                ),
    +                ["warnings", "functools"],
    +                InterpreterError("Forbidden access to module: sys"),
    +            ),
    +            # Using map
    +            (
    +                dedent(
    +                    """
    +                    import warnings
    +                    list(map(getattr, [warnings], ["sys"]))
    +                    """
    +                ),
    +                ["warnings"],
    +                InterpreterError("Forbidden access to module: sys"),
    +            ),
    +            # Using map with functools.partial
    +            (
    +                dedent(
    +                    """
    +                    import functools
    +                    import warnings
    +                    list(map(functools.partial(getattr, warnings), ["sys"]))
    +                    """
    +                ),
    +                ["warnings", "functools"],
    +                InterpreterError("Forbidden access to module: sys"),
    +            ),
    +        ],
    +    )
    +    def test_vulnerability_via_submodules_through_indirect_attribute_access(
    +        self, code, additional_authorized_imports, expected_error
    +    ):
    +        # warnings.sys
    +        executor = LocalPythonExecutor(additional_authorized_imports)
    +        executor.send_tools({})
    +        with pytest.raises(type(expected_error), match=f".*{expected_error}"):
    +            executor(code)
    +
         @pytest.mark.parametrize(
             "additional_authorized_imports, additional_tools, expected_error",
             [
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.