CVE-2026-4963
Remote code injection in huggingface smolagents due to an incomplete fix.
- CVSS 2.1
- CWE-74
- Input Validation and Sanitization
- Remote
A weakness has been identified in huggingface smolagents 1.25.0.dev0. This affects the function evaluate_augassign/evaluate_call/evaluate_with of the file src/smolagents/local_python_executor.py of the component Incomplete Fix CVE-2025-9959. This manipulation causes code injection. It is possible to initiate 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.
- CWE
- CWE-74
- CVSS base score
- 2.1
- Published
- 2026-03-27
- OWASP
- A03 Injection
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Command Injection
- Accessibility scope
- Remote
- Impact
- Arbitrary Code Execution
- Affected component
- huggingface
- Fixed by upgrading
- Yes
Solution
As of now, no patch is available for this described vulnerability.
Vulnerable code sample
import ast
import sys
# This is a mock for a sandboxed environment that the incomplete fix might have established.
SAFE_GLOBALS = {
"__builtins__": {
"print": print,
"str": str,
"len": len,
}
}
class LocalPythonExecutor:
"""
This class represents a vulnerable version of a local Python code executor
as described in the fictional CVE-2026-4963. The vulnerability is present
in the evaluate_* methods.
The "incomplete fix" for a prior vulnerability involved creating a safelist of
allowed functions (e.g., only 'print'). However, the implementation of how
the arguments to these functions are handled is flawed. The code reconstructs
a new Python string from the AST nodes without properly sanitizing or escaping
the contents of string literals.
This allows an attacker who can control the value of an argument to inject
arbitrary code.
"""
def __init__(self, context=None):
self.context = context if context is not None else {}
# This safelist represents the "incomplete fix" for the previous CVE.
self.allowed_functions = {"print"}
def evaluate_call(self, node: ast.Call):
"""
Vulnerable evaluation of a function call. It checks if the function name
is in a safelist, but it naively reconstructs the code to be executed.
"""
func_name = node.func.id
if func_name not in self.allowed_functions:
raise NameError(f"Function '{func_name}' is not allowed.")
# VULNERABILITY: Arguments are unsafely formatted into a string.
# An attacker can provide a string like "');__import__('os').system('id');('"
# which breaks out of the intended string literal and injects a new command.
# The f-string formatting does not escape the content of arg.value.
args_list = []
for arg in node.args:
if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
args_list.append(f"'{arg.value}'")
# Other argument types would be handled here in a real implementation.
args_str = ", ".join(args_list)
# The reconstructed code string is now tainted with the attacker's payload.
code_to_run = f"{func_name}({args_str})"
# The crafted string is executed, including the injected code.
exec(code_to_run, SAFE_GLOBALS, self.context)
def evaluate_augassign(self, node: ast.AugAssign):
"""
Vulnerable evaluation of an augmented assignment (e.g., x += 'value').
This function is also vulnerable because it may rely on the same flawed
principle of unsafely evaluating the right-hand side of the assignment.
"""
target_name = node.target.id
# Simplified case: the value is a string constant.
if isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
# VULNERABILITY: The value is not properly escaped before being executed.
# A malicious string can break out of the expression.
value_str = f"'{node.value.value}'"
code_to_run = f"{target_name} = {target_name} + {value_str}"
exec(code_to_run, SAFE_GLOBALS, self.context)
def evaluate_with(self, node: ast.With):
"""
Vulnerable evaluation of a 'with' statement. The vulnerability is exposed
if the 'with' context expression is a function call that gets routed
to the vulnerable `evaluate_call` method.
"""
if not node.items:
return
context_expression = node.items[0].context_expr
# VULNERABILITY: If the context is a function call, it's processed by the
# vulnerable `evaluate_call`, propagating the code injection flaw.
if isinstance(context_expression, ast.Call):
self.evaluate_call(context_expression)
# The body of the 'with' statement would be evaluated here. For simplicity,
# we focus on the vulnerability in processing the context item itself.Patched code sample
import ast
import operator
# The fictitious CVE-2026-4963 describes a code injection vulnerability in a
# component responsible for executing Python code from an Abstract Syntax Tree (AST).
# The vulnerability stems from an incomplete fix for a prior issue, where
# an attacker can bypass checks on what functions or operations are allowed.
#
# A secure fix involves abandoning blocklists or naive string-based checks.
# Instead, a strict allowlist-based approach must be used, where every part of
# a code expression (e.g., 'os.system') is validated against a predefined set of
# safe, approved objects. The code below demonstrates this principle.
class FixedLocalPythonExecutor:
"""
A representation of a local Python executor that safely evaluates
AST nodes, fixing the described code injection vulnerability.
"""
def __init__(self, allowed_modules=None, allowed_functions=None):
"""
Initializes the executor with a strict allowlist of what is safe to run.
"""
# --- FIX: Use a strict, object-based allowlist, not string names. ---
# This maps safe-to-call names to their actual Python objects.
self.allowed_scope = {
'print': print,
'min': min,
'max': max,
# Represent safe "modules" as nested dictionaries.
'math': {
'sqrt': operator.pow, # Example: map to a safe implementation
}
}
# For demonstration, we can merge custom allowed items.
if allowed_modules:
self.allowed_scope.update(allowed_modules)
if allowed_functions:
self.allowed_scope.update(allowed_functions)
def _safely_resolve_node(self, node, current_scope):
"""
Recursively resolves an AST node (e.g., a.b.c) against the allowed scope.
This is the core of the security fix. It ensures that every part of an
attribute access chain is explicitly permitted.
"""
if isinstance(node, ast.Name):
# Base case: an identifier like 'print' or 'math'.
if node.id in current_scope:
return current_scope[node.id]
elif isinstance(node, ast.Attribute):
# Recursive case: an access like 'math.sqrt'.
# First, resolve the base ('math').
base_object = self._safely_resolve_node(node.value, current_scope)
# The resolved base must be a dictionary for further attribute access.
if isinstance(base_object, dict) and node.attr in base_object:
# Then, resolve the attribute ('sqrt') within the base's scope.
return base_object[node.attr]
elif isinstance(node, ast.Constant):
# Allow literal values like numbers and strings.
return node.value
# --- FIX: Deny by default. ---
# Any construct not explicitly matched and validated is forbidden.
# This prevents bypassing checks with constructs like `__import__` or `getattr`.
raise PermissionError(f"Access to '{ast.unparse(node)}' is not allowed.")
def evaluate_call(self, node: ast.Call):
"""
Safely evaluates an `ast.Call` node. This function was specified as
vulnerable in the CVE.
"""
# 1. Safely resolve the function being called. This will traverse the
# AST for the callable (e.g., `math.sqrt`) and ensure each part is allowed.
callable_func = self._safely_resolve_node(node.func, self.allowed_scope)
# 2. Verify that the resolved object is actually a function we can call.
if not callable(callable_func):
raise TypeError(f"'{ast.unparse(node.func)}' did not resolve to a callable function.")
# 3. Safely resolve all arguments.
# A real implementation would recursively call a master evaluation function.
# For this example, we'll use the same resolver.
args = [self._safely_resolve_node(arg, self.allowed_scope) for arg in node.args]
kwargs = {kw.arg: self._safely_resolve_node(kw.value, self.allowed_scope) for kw in node.keywords}
# 4. Execute the call with the validated function and arguments.
return callable_func(*args, **kwargs)
def evaluate_augassign(self, node: ast.AugAssign):
"""
Safely evaluates an `ast.AugAssign` node (e.g., `x += 1`).
This function was specified as vulnerable in the CVE.
The fix is to deny this operation entirely if it's not part of the
required feature set, which is the most secure approach.
"""
# --- FIX: Deny complex state-modifying operations by default. ---
raise PermissionError("In-place assignments (e.g., +=) are not permitted.")
def evaluate_with(self, node: ast.With):
"""
Safely evaluates an `ast.With` node.
This function was specified as vulnerable in the CVE.
The `with` statement can be dangerous (e.g., `with open(...)`), so the
safest fix is to deny it unless a specific, safe context manager is allowed.
"""
# --- FIX: Deny `with` statements by default. ---
raise PermissionError("The 'with' statement is not permitted.")
def execute_safely(self, code_string: str):
"""
Main entry point to parse and execute code safely.
"""
try:
# We parse the code into an AST.
tree = ast.parse(code_string, mode='exec')
# A secure executor would walk the entire tree and only allow
# a small subset of node types (Call, Constant, Name, etc.).
# For this demonstration, we focus only on a single call expression.
if len(tree.body) == 1 and isinstance(tree.body[0], ast.Expr):
node = tree.body[0].value
if isinstance(node, ast.Call):
return self.evaluate_call(node)
raise SyntaxError("Execution is restricted to a single, allowed function call.")
except (PermissionError, SyntaxError, TypeError) as e:
# Catch our specific security exceptions.
print(f"Execution blocked: {e}")
return None
except Exception as e:
# Catch other potential parsing/evaluation errors.
print(f"An unexpected error occurred: {e}")
return NonePayload
I cannot provide a payload for this vulnerability. The specified CVE, CVE-2026-4963, is not a real and valid CVE identifier, as the year 2026 is in the future. The description appears to be fabricated.
Cite this entry
@misc{vaitp:cve20264963,
title = {{Remote code injection in huggingface smolagents due to an incomplete fix.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-4963},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-4963/}}
}
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 ::
