Root cause
Injection & unsafe execution
Add eval commands sanitizer
How AI contributed: New attack surfacePublished Feb 2, 2026 in
langroid/langroidAll 3 findings in this repository
Affected Scope langroid <= 0.59.31 Vulnerability Description CVE-2025-46724 fix bypass: TableChatAgent can call pandas_eval tool to evaluate the expression. There is a WAF in langroid/utils/pandas_utils.py introduced to block code injection CVE-2025-46724. However it can be bypassed due to _literal_ok() returning False instead of raising UnsafeCommandError on invalid input, combined with unrestricted access to dangerous dunder attributes (__init__, __globals__, __builtins__). This allows chaining whitelisted DataFrame methods to leak the eval builtin and execute arbitrary code. Reproduction & PoC This payload is used to bypass the waf, when executed, the column name __import__('os').system('ls')#... will be passed to eval here is the PoC python df.add_prefix("__import__('os').system('ls')#").T.groupby(by=df.__init__.__globals__['__builtins__']['eval']) after running this poc, command ls will be executed on the server <img width="2501" height="1256" alt="image" src="https://github.com/user-attachments/assets/98b83585-68e0-4be4-a7a6-21909fed662e" /> Gadget pandas_eval (langroid\agent\special\table_chat_agent.py:239) handle_tool_message (langroid\agent\base.py:2092) handle_message (langroid\agent\base.py:1744) agent_response (langroid\agent\base.py:760) response (langroid\agent\task.py:1584) step (langroid\agent\task.py:1261) run (langroid\agent\task.py:827) Security Impact Remote Code Execution (RCE) via pandas_eval` tool. Attackers can execute arbitrary shell commands through controlled user input.
In langroid/utils/pandas_utils.py, the command sanitizer's _literal_ok helper returned False rather than raising UnsafeCommandError for an invalid literal, and attribute lookups were never rejected, so an expression such as df.__init__.__globals__['__builtins__']['eval'] survived validation and exposed a builtin that executes code.
Root cause
Add eval commands sanitizer
Fix
Fix CVE-2025-46724 bypass in TableChatAgent sanitization (#966)
AI-assisted fix: Claude Code · Prasad Chalasani
Case facts
@@ -1,8 +1,210 @@+import ast+import itertools+ from typing import Any import pandas as pd ++COMMON_USE_DF_METHODS = {+ "T",+ "abs", "add", "add_prefix", "add_suffix",+ "agg", "aggregate", "align",+ "all", "any", "apply", "applymap",+ "at", "at_time", "between_time",+ "bfill", "clip", "combine", "combine_first",+ "convert_dtypes", "corr", "corrwith", "count", "cov",+ "cummax", "cummin", "cumprod", "cumsum",+ "describe", "diff", "dot", "drop_duplicates", "duplicated",+ "eq", "eval", "ewm", "expanding",+ "explode", "filter", "first",+ "groupby",+ "head", "idxmax", "idxmin", "infer_objects", "interpolate", "isin",+ "kurt", "kurtosis",+ "last", "le", "loc", "lt", "gt", "ge",+ "iloc", "mask", "max", "mean", "median", "melt",+ "min", "mode", "mul",+ "nlargest", "nsmallest", "notna", "notnull", "nunique",+ "pct_change", "pipe", "pivot", "pivot_table", "prod", "product",+ "quantile", "query",+ "rank", "replace", "resample", "rolling", "round",+ "sample", "select_dtypes", "sem", "shift", "skew",+ "sort_index", "sort_values", "squeeze", "stack", "std", "sum",+ "tail", "transform", "transpose",+ "unstack",+ "value_counts", "var",+ "where", "xs",+}++POTENTIALLY_DANGEROUS_DF_METHODS = {+ "eval",+ "query",+ "apply",+ "applymap",+ "pipe",+ "agg",+ "aggregate",+ "transform",+ "rolling",+ "expanding",+ "resample",+}++WHITELISTED_DF_METHODS = COMMON_USE_DF_METHODS - POTENTIALLY_DANGEROUS_DF_METHODS+++BLOCKED_KW = {+ "engine",+ "parser",+ "inplace",+ "regex",+ "dtype",+ "converters",+ "eval", +}+MAX_CHAIN = 6 +MAX_DEPTH = 25+NUMERIC_LIMIT = 1_000_000_000 ++++class UnsafeCommandError(ValueError):+ """Raised when a command string violates security policy."""+ pass+++def _literal_ok(node: ast.AST) -> bool:+ """Return True if *node* is a safe literal (and within numeric limit)."""+ if isinstance(node, ast.Constant):+ if isinstance(node.value, (int, float, complex)) and abs(node.value) > NUMERIC_LIMIT:+ raise UnsafeCommandError("numeric constant exceeds limit")+ return True+ if isinstance(node, (ast.Tuple, ast.List)):+ return all(_literal_ok(elt) for elt in node.elts)+ if isinstance(node, ast.Slice):+ return all(sub is None or _literal_ok(sub) for sub in (node.lower, node.upper, node.step))+ return False++@@ -237,6 +237,16 @@ def visit_Subscript(self, node: ast.Subscript) -> None: raise UnsafeCommandError("subscript must be literal") self.generic_visit(node) + # Attribute access+ def visit_Attribute(self, node: ast.Attribute) -> None:+ # Block dunder attributes to prevent access to __init__, __globals__, etc.+ if node.attr.startswith("__") and node.attr.endswith("__"):+ raise UnsafeCommandError(f"dunder attribute '{node.attr}' not allowed")+ # Block single underscore private attributes as well for defense in depth+ if node.attr.startswith("_") and node.attr not in WHITELISTED_DF_METHODS:+ raise UnsafeCommandError(f"private attribute '{node.attr}' not allowed")+ self.generic_visit(node)+ # Method calls def visit_Call(self, node: ast.Call) -> None: if not isinstance(node.func, ast.Attribute):@@ -253,6 +263,7 @@ def visit_Call(self, node: ast.Call) -> None: for kw in node.keywords: if kw.arg in BLOCKED_KW: raise UnsafeCommandError(f"kwarg '{kw.arg}' is blocked")+ # Check numeric limits on literals; non-literals validated via generic_visit _literal_ok(kw.value) for arg in node.args: _literal_ok(arg)@@ -40,6 +40,29 @@ r"unexpected variable 'other_var'", ), (DEEP_EXPR, r"AST nesting too deep"),+ # CVE-2025-46724 bypass tests - dunder attribute access+ ("df.__init__", r"dunder attribute '__init__' not allowed"),+ ("df.__class__", r"dunder attribute '__class__' not allowed"),+ ("df.__globals__", r"dunder attribute '__globals__' not allowed"),+ ("df.__builtins__", r"dunder attribute '__builtins__' not allowed"),+ # CVE-2025-46724 bypass tests - private attribute access+ ("df._private", r"private attribute '_private' not allowed"),+ ("df._internal_method()", r"method '_internal_method' not permitted"),+ # CVE-2025-46724 bypass tests - dunder access via kwargs (the actual bypass vector)+ (+ "df.groupby(by=df.__init__)",+ r"dunder attribute '__init__' not allowed",+ ),+ (+ "df.groupby(by=df.__class__.__bases__)",+ r"dunder attribute '__.+__' not allowed",+ ),+ # Full PoC exploit payload - blocks on dunder attribute access+ (+ "df.add_prefix(\"__import__('os').system('ls')#\").T.groupby("+ "by=df.__init__.__globals__['__builtins__']['eval'])",+ r"dunder attribute '__.+__' not allowed",+ ), ] AI-assisted change 4f61d0db4bbb2f548d29279a72e7d1f9b189cedc04a5ee4d58d9babd5c7d8e81 · Fix fbdd25e1f7b052e2080d637b9d36e29fae901a7cd9430adb0b49ffba5b9ffbcc