VAITP Dataset

← Back to the dataset

CVE-2026-2654

SSRF vulnerability in smolagents 1.24.0's LocalPythonExecutor component.

  • CVSS 2.1
  • CWE-918
  • Input Validation and Sanitization
  • Remote

A weakness has been identified in huggingface smolagents 1.24.0. Impacted is the function requests.get/requests.post of the component LocalPythonExecutor. Executing a manipulation can lead to server-side request forgery. It is possible to launch the attack remotely. The exploit has been made available to the public and could be used for attacks. The vendor was contacted early about this disclosure but did not respond in any way.

CVSS base score
2.1
Published
2026-02-18
OWASP
A10 Server-Side Request Forgery (SSRF)
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Input Validation and Sanitization
Subcategory
Server-Side Request Forgery (SSRF)
Accessibility scope
Remote
Impact
Information Disclosure
Affected component
huggingface
Fixed by upgrading
Yes

Solution

Upgrade to smol-ai version 0.0.25 or later.

Vulnerable code sample

import requests
import sys
from io import StringIO

class LocalPythonExecutor:
    """
    A component designed to execute Python code snippets locally.
    This version is vulnerable to code injection, leading to SSRF.
    """
    def __init__(self):
        # In a real-world scenario, this might set up execution environments.
        # The vulnerability lies in the lack of sandboxing.
        pass

    def execute(self, code_to_run: str):
        """
        Executes a given string of Python code. The CVE description implies
        that user-controllable data can be crafted into the `code_to_run`
        parameter. The function does not validate or sanitize the code,
        allowing calls like `requests.get` or `requests.post` to target
        internal network resources.
        """
        # The available modules are provided to the exec scope.
        # An attacker can use the `requests` module to initiate SSRF.
        available_modules = {'requests': requests}

        # Redirect stdout to capture the output of the executed code.
        old_stdout = sys.stdout
        redirected_output = sys.stdout = StringIO()

        try:
            # The core of the vulnerability: Unsafe execution of arbitrary code.
            # An attacker can pass a string like:
            # "print(requests.get('http://169.254.169.254/latest/meta-data/').text)"
            # to exploit this.
            exec(code_to_run, available_modules)
        except Exception as e:
            sys.stdout = old_stdout
            return f"An error occurred: {e}"
        finally:
            # Restore stdout
            sys.stdout = old_stdout

        return redirected_output.getvalue()

Patched code sample

import requests
from urllib.parse import urlparse
import ipaddress


class LocalPythonExecutorFixed:
    """
    A class to execute Python code with protections against Server-Side
    Request Forgery (SSRF) vulnerabilities.

    The fix addresses the described vulnerability by not exposing the `requests`
    module directly to the executed code. Instead, it provides wrapped, safer
    versions of `get` and `post` functions that enforce security checks.
    """

    def __init__(self, allowed_domains=None):
        """
        Initializes the executor with a set of allowed domains for network requests.

        Args:
            allowed_domains (set, optional): A set of strings representing allowed
                                             hostnames. Defaults to an empty set.
        """
        self.allowed_domains = allowed_domains or set()
        
        # The 'safe_globals' dictionary defines the limited, secure environment
        # in which the user-provided code will be executed.
        # Crucially, it does NOT include the 'requests' module directly.
        self.safe_globals = {
            "__builtins__": {
                "print": print,
                "range": range,
                "len": len,
                "str": str,
                "int": int,
                "float": float,
                "list": list,
                "dict": dict,
                "set": set,
                "True": True,
                "False": False,
                "None": None,
            },
            "safe_get": self._safe_requests_get,
            "safe_post": self._safe_requests_post,
        }

    def _is_url_allowed(self, url: str) -> bool:
        """
        Validates a URL against a set of security policies to prevent SSRF.

        Checks performed:
        1. The URL scheme must be HTTP or HTTPS.
        2. The hostname must not be a private/internal or reserved IP address.
        3. The hostname must be in the predefined `allowed_domains` list.

        Args:
            url (str): The URL to validate.

        Returns:
            bool: True if the URL is allowed, False otherwise.
        """
        try:
            parsed_url = urlparse(url)

            # 1. Enforce allowed schemes
            if parsed_url.scheme not in ["http", "https"]:
                return False

            hostname = parsed_url.hostname
            if not hostname:
                return False

            # 2. Resolve hostname to IP and check if it's a private/reserved address
            # This helps prevent DNS rebinding attacks and direct calls to internal IPs.
            try:
                ip_addr = ipaddress.ip_address(requests.utils.get_host_ip_address(hostname))
                if ip_addr.is_private or ip_addr.is_loopback or ip_addr.is_reserved:
                    return False
            except Exception:
                # If hostname resolution fails, deny the request.
                return False

            # 3. Check against the domain allowlist
            if hostname not in self.allowed_domains:
                return False

            return True

        except (ValueError, AttributeError):
            # Catches malformed URLs or other parsing errors
            return False

    def _safe_requests_get(self, url, **kwargs):
        """
        A secure wrapper for `requests.get`. It enforces the URL allowlist
        and removes potentially dangerous arguments.
        """
        if self._is_url_allowed(url):
            # Sanitize kwargs to prevent security risks like proxy injection
            kwargs.pop("proxies", None)
            kwargs.setdefault("timeout", 5)  # Enforce a timeout
            return requests.get(url, **kwargs)
        else:
            raise PermissionError(f"SSRF Detected: Access to URL '{url}' is forbidden by policy.")

    def _safe_requests_post(self, url, **kwargs):
        """
        A secure wrapper for `requests.post`. It enforces the URL allowlist
        and removes potentially dangerous arguments.
        """
        if self._is_url_allowed(url):
            # Sanitize kwargs
            kwargs.pop("proxies", None)
            kwargs.setdefault("timeout", 5)
            return requests.post(url, **kwargs)
        else:
            raise PermissionError(f"SSRF Detected: Access to URL '{url}' is forbidden by policy.")

    def execute_code(self, code_string: str):
        """
        Executes a string of Python code in a sandboxed environment.

        The vulnerability is fixed by providing a restricted scope to the `exec`
        function. Malicious code attempting to use `requests.get` will raise a
        `NameError`, and code using `safe_get` with a disallowed URL will
        raise a `PermissionError`.

        Args:
            code_string (str): The Python code to execute.
        """
        try:
            # Execute the code with the safe, limited global scope.
            exec(code_string, self.safe_globals, {})
        except Exception as e:
            # In a real application, this would be logged securely.
            print(f"Code execution failed: {type(e).__name__}: {e}")

Payload

import requests; requests.get('http://169.254.169.254/latest/meta-data/')

Cite this entry

@misc{vaitp:cve20262654,
  title        = {{SSRF vulnerability in smolagents 1.24.0's LocalPythonExecutor component.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-2654},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-2654/}}
}
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 ::