CVE-2025-46724
Langroid `TableChatAgent` vulnerable to code injection via `pandas eval()`.
- CVSS 9.8
- CWE-94
- Input Validation and Sanitization
- Remote
Langroid is a Python framework to build large language model (LLM)-powered applications. Prior to version 0.53.15, `TableChatAgent` uses `pandas eval()`. If fed by untrusted user input, like the case of a public-facing LLM application, it may be vulnerable to code injection. Langroid 0.53.15 sanitizes input to `TableChatAgent` by default to tackle the most common attack vectors, and added several warnings about the risky behavior in the project documentation.
- CWE
- CWE-94
- CVSS base score
- 9.8
- Published
- 2025-05-20
- OWASP
- A03 Injection
- Orthogonal defect classification
- Interface
- Code defect classification
- Incorrect Functionality
- Category
- Input Validation and Sanitization
- Subcategory
- Insecure Parsing or Deserialization
- Accessibility scope
- Remote
- Impact
- Arbitrary Code Execution
- Affected component
- Langroid
- Fixed by upgrading
- Yes
Solution
Upgrade to version 0.53.15 or later.
Vulnerable code sample
import pandas as pd
class TableChatAgent:
def __init__(self, df):
"""Vulnerable function that demonstrates the security issue."""
self.df = df
def query_table(self, query):
"""
Queries the table using pandas eval(). Potentially vulnerable.
"""
try:
result = self.df.eval(query)
return result
except Exception as e:
return f"Error: {e}"
# Example usage (BEFORE fix - VULNERABLE)
if __name__ == '__main__':
data = {'col1': [1, 2, 3], 'col2': [4, 5, 6]}
df = pd.DataFrame(data)
agent = TableChatAgent(df)
# User input - VERY DANGEROUS - This is just an example, NEVER pass unsanitized user input directly to eval()
user_query = "df.col1.sum()" # or something malicious like "__import__('os').system('rm -rf /')"
response = agent.query_table(user_query)
print(response)Patched code sample
import pandas as pd
import ast
import operator as op
# Define a safe list of allowed operators
SAFE_OPERATORS = {
ast.Add: op.add,
ast.Sub: op.sub,
ast.Mult: op.mul,
ast.Div: op.truediv,
ast.FloorDiv: op.floordiv,
ast.Mod: op.mod,
ast.Pow: op.pow,
ast.BitXor: op.xor,
ast.USub: op.neg,
ast.Eq: op.eq,
ast.NotEq: op.ne,
ast.Lt: op.lt,
ast.LtE: op.le,
ast.Gt: op.gt,
ast.GtE: op.ge,
ast.And: op.and_,
ast.Or: op.or_,
}
def safe_eval(expression, data):
"""
Safely evaluates a mathematical expression using ast.literal_eval and allowed operators.
Protects against code injection vulnerabilities.
Args:
expression (str): The mathematical expression to evaluate.
data (dict): Dictionary of data available to expression
Returns:
The result of the evaluation. Raises ValueError if the expression is unsafe.:
"""
node = ast.parse(expression, mode='eval').body
def eval_(node):
"""Secure function that fixes the vulnerability."""
if isinstance(node, ast.Num):
return node.n
elif isinstance(node, ast.Name):
# Check if variable is in the provided data:
if node.id in data:
return data[node.id]
else:
raise ValueError(f"Variable '{node.id}' is not defined.")
elif isinstance(node, ast.BinOp):
try:
return SAFE_OPERATORS[type(node.op)](eval_(node.left), eval_(node.right))
except KeyError:
raise ValueError("Unsafe operator used.")
elif isinstance(node, ast.UnaryOp):
try:
return SAFE_OPERATORS[type(node.op)](eval_(node.operand))
except KeyError:
raise ValueError("Unsafe operator used.")
elif isinstance(node, ast.Compare):
left = eval_(node.left)
comparisons = []
for op_node, right_node in zip(node.ops, node.comparators):
try:
op_func = SAFE_OPERATORS[type(op_node)]
except KeyError:
raise ValueError("Unsafe operator used.")
right = eval_(right_node)
comparisons.append(op_func(left, right))
left = right # left becomes right for chained comparisons:
# All comparisons must be true for the whole expression to be true:
return all(comparisons)
elif isinstance(node, ast.BoolOp):
if isinstance(node.op, ast.And):
op_func = SAFE_OPERATORS[ast.And]
values = [eval_(value) for value in node.values]:
result = True
for value in values:
result = op_func(result, value)
if not result: # Short-circuit evaluation for 'and':
return result
return result
elif isinstance(node.op, ast.Or):
op_func = SAFE_OPERATORS[ast.Or]
values = [eval_(value) for value in node.values]:
result = False
for value in values:
result = op_func(result, value)
if result: # Short-circuit evaluation for 'or':
return result
return result
else:
raise ValueError("Unsafe boolean operator used.")
else:
raise ValueError("Unsafe expression type.")
return eval_(node)
class TableChatAgent:
def __init__(self, df):
"""Secure function that fixes the vulnerability."""
self.df = df
def process_query(self, query):
"""
Processes a query against the DataFrame, using safe evaluation.
"""
try:
# Example: Using safe_eval to perform calculations on a column
# Assuming the query is something like "column1 + column2 > 10"
# and column1 and column2 exists as columns on the dataframe.
# The data dictionary is now only columns that actually exist.
# Not the entire dataframe.
available_cols = self.df.columns
data = {}
for col in available_cols:
data[col] = self.df[col]
result = safe_eval(query, data)
return result
except ValueError as e:
return f"Error: {e}" # Errors are safe
except Exception as e:
return f"An unexpected error occurred: {e}"
# Example usage
if __name__ == '__main__':
data = {'col1': [1, 2, 3], 'col2': [4, 5, 6]}
df = pd.DataFrame(data)
agent = TableChatAgent(df)
# Safe query
safe_query = "col1 + col2"
print(f"Safe Query: {safe_query}, Result: {agent.process_query(safe_query)}")
# Safe query with comparison
safe_query_comparison = "col1 + col2 > 7"
print(f"Safe Query: {safe_query_comparison}, Result: {agent.process_query(safe_query_comparison)}")
# Unsafe query (attempt to access external function)
unsafe_query = "__import__('os').system('echo PWNED')" # Attempt at code injection
print(f"Unsafe Query: {unsafe_query}, Result: {agent.process_query(unsafe_query)}") # Should return error
# Unsafe query using exec
unsafe_query_exec = "exec('print(\"PWNED!\")')"
print(f"Unsafe Query: {unsafe_query_exec}, Result: {agent.process_query(unsafe_query_exec)}") # Should return errorPayload
__import__('os').system('touch /tmp/pwned')
Cite this entry
@misc{vaitp:cve202546724,
title = {{Langroid `TableChatAgent` vulnerable to code injection via `pandas eval()`.
}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2025},
note = {VAITP Python Vulnerability Dataset, entry CVE-2025-46724},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-46724/}}
}
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 ::
