Root cause
Injection & unsafe execution
fixes to code-injection mitigation PR #841 (#850)
AI-assisted change: GitHub Copilot
How AI contributed: Flawed AI-written codePublished Jul 6, 2026 in
langroid/langroidAll 3 findings in this repository
CVE-2026-54769/GHSA-q9p7-wqxg-mrhc: TableChatAgent.pandas_eval and VectorStore.compute_from_docs evaluate LLM-generated expressions with eval(code, vars, {}) where the empty locals dict provides false security - Python implicitly injects builtins into globals, so an expression like __import__('os').system(...) executes (full_eval=True path) -> RCE. Fixed in 0.65.2 by 37b2fbbc adding safe_eval_globals restricting builtins in langroid/utils/pandas_utils.py.
Root cause
fixes to code-injection mitigation PR #841 (#850)
AI-assisted change: GitHub Copilot
Fix
Adds a curated builtins dictionary and passes it explicitly to each eval of generated pandas expressions, preventing Python from exposing dangerous builtins such as __import__, eval, exec, and open.
Fix by Prasad Chalasani · no AI marker found
Case facts
@@ -14,7 +14,7 @@ from langroid.utils.configuration import settings from langroid.utils.object_registry import ObjectRegistry from langroid.utils.output.printing import print_long_text-from langroid.utils.pandas_utils import stringify+from langroid.utils.pandas_utils import sanitize_command, stringify from langroid.utils.pydantic_utils import flatten_dict logger = logging.getLogger(__name__)@@ -159,11 +159,12 @@ def compute_from_docs(self, docs: List[Document], calc: str) -> str: df = pd.DataFrame(dicts) try:- # SECURITY: Use Python's eval() with NO globals and only {"df": df}- # in locals. This allows pandas operations on `df` while preventing- # access to builtins or other potentially harmful global functions,- # mitigating risks associated with executing untrusted `calc` strings.- result = eval(calc, {}, {"df": df}) # type: ignore+ # SECURITY MITIGATION: Eval input is sanitized to prevent most common+ # code injection attack vectors.+ vars = {"df": df}+ calc = sanitize_command(calc)+ code = compile(calc, "<calc>", "eval")+ result = eval(code, vars, {}) except Exception as e: # return error message so LLM can fix the calc string if needed err = f"""@@ -1,7 +1,287 @@+import ast 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",@@ -7,6 +7,13 @@ The expression is passed via the `pandas_eval` tool/function-call, which is handled by the Agent's `pandas_eval` method. This method evaluates the expression and returns the result as a string.++WARNING: This Agent should be used only with trusted input, as it can execute system+commands. ++The `full_eval` flag is false by default, which means that the input is sanitized+against most common code injection attack vectors. `full_eval` may be set to True to +disable sanitization at all. Both cases should be used with caution. """ import io@@ -26,6 +33,7 @@ from langroid.parsing.table_loader import read_tabular_data from langroid.prompts.prompts_config import PromptsConfig from langroid.utils.constants import DONE, PASS+from langroid.utils.pandas_utils import sanitize_command from langroid.vector_store.base import VectorStoreConfig logger = logging.getLogger(__name__)@@ -113,6 +121,9 @@ class TableChatAgentConfig(ChatAgentConfig): cache: bool = True # cache results debug: bool = False stream: bool = True # allow streaming where needed+ full_eval: bool = (+ False # runs eval without sanitization. Use only on trusted input!+ ) data: str | pd.DataFrame # data file, URL, or DataFrame separator: None | str = None # separator for data file vecdb: None | VectorStoreConfig = None@@ -204,18 +215,21 @@ def pandas_eval(self, msg: PandasEvalTool) -> str: """ self.sent_expression = True exprn = msg.expression- local_vars = {"df": self.df}+ vars = {"df": self.df} # Create a string-based I/O stream code_out = io.StringIO() # Temporarily redirect standard output to our string-based I/O stream sys.stdout = code_out # Evaluate the last line and get the result;- # SECURITY: eval only with empty globals and {"df": df} in locals to- # prevent arbitrary Python code execution.+ # SECURITY MITIGATION: Eval input is sanitized by default to prevent most+ # common code injection attack vectors. try:- eval_result = eval(exprn, {}, local_vars)+ if not self.config.full_eval:+ exprn = sanitize_command(exprn)+ code = compile(exprn, "<calc>", "eval")+ eval_result = eval(code, vars, {}) except Exception as e: eval_result = f"ERROR: {type(e)}: {e}" @@ -226,7 +240,7 @@ def pandas_eval(self, msg: PandasEvalTool) -> str: sys.stdout = sys.__stdout__ # If df has been modified in-place, save the changes back to self.df- self.df = local_vars["df"]+ self.df = vars["df"] # Get the resulting string from the I/O stream print_result = code_out.getvalue() or ""@@ -14,7 +14,7 @@ from langroid.utils.configuration import settings from langroid.utils.object_registry import ObjectRegistry from langroid.utils.output.printing import print_long_text-from langroid.utils.pandas_utils import sanitize_command, stringify+from langroid.utils.pandas_utils import safe_eval_globals, sanitize_command, stringify from langroid.utils.pydantic_utils import flatten_dict logger = logging.getLogger(__name__)@@ -217,12 +217,16 @@ def compute_from_docs(self, docs: List[Document], calc: str) -> str: try: # SECURITY MITIGATION: Eval input is sanitized to prevent most common- # code injection attack vectors when full_eval is False.+ # code injection attack vectors when full_eval is False. The globals+ # dict also restricts ``__builtins__`` so that even with+ # ``full_eval=True`` the expression cannot reach+ # ``__import__``/``eval``/``exec`` via Python's implicit builtin+ # injection (GHSA-q9p7-wqxg-mrhc). vars = {"df": df} if not self.config.full_eval: calc = sanitize_command(calc) code = compile(calc, "<calc>", "eval")- result = eval(code, vars, {})+ result = eval(code, safe_eval_globals(vars), {}) except Exception as e: # return error message so LLM can fix the calc string if needed err = f"""@@ -1,5 +1,5 @@ import ast-from typing import Any+from typing import Any, Dict import pandas as pd @@ -295,6 +295,59 @@ def sanitize_command(expr: str, df_name: str = "df") -> str: return expr +# Curated ``__builtins__`` dict for ``eval()`` calls on LLM-generated pandas+# expressions (GHSA-q9p7-wqxg-mrhc). Without this, Python implicitly injects+# the full builtins into eval's globals -- so even with an empty ``locals={}``+# and ``full_eval=True``, an expression like+# ``__import__('os').system('...')`` would execute and yield RCE. Restricting+# builtins to this curated read-only set closes the direct primitive while+# leaving enough functionality for typical pandas expressions.+_SAFE_BUILTINS: Dict[str, Any] = {+ # Constants+ "True": True,+ "False": False,+ "None": None,+ # Safe builtins occasionally used in pandas-style expressions+ "abs": abs,+ "all": all,+ "any": any,+ "bool": bool,+ "dict": dict,+ "enumerate": enumerate,+ "float": float,+ "int": int,+ "len": len,+ "list": list,+ "max": max,+ "min": min,+ "range": range,+ "reversed": reversed,+ "round": round,+ "set": set,+ "sorted": sorted,+ "str": str,+ "sum": sum,+ "tuple": tuple,+ "zip": zip,+}+++def safe_eval_globals(local_vars: Dict[str, Any]) -> Dict[str, Any]:+ """Return a globals dict for :func:`eval` with ``__builtins__`` restricted+ to a safe read-only set, so that LLM-generated expressions cannot reach+ ``__import__``, ``eval``, ``exec``, ``open``, etc. via Python's implicit+ builtin injection.++ Args:+ local_vars: User-provided variables to expose to the eval'd+ expression (e.g. ``{"df": <DataFrame>}``).++ Returns:+ A new dict suitable as the ``globals`` argument of ``eval()``.+ """+ return {**local_vars, "__builtins__": _SAFE_BUILTINS}++ def stringify(x: Any) -> str: # Convert x to DataFrame if it is not one already if isinstance(x, pd.Series):@@ -33,7 +33,7 @@ from langroid.parsing.table_loader import read_tabular_data from langroid.prompts.prompts_config import PromptsConfig from langroid.utils.constants import DONE, PASS-from langroid.utils.pandas_utils import sanitize_command+from langroid.utils.pandas_utils import safe_eval_globals, sanitize_command from langroid.vector_store.base import VectorStoreConfig logger = logging.getLogger(__name__)@@ -231,12 +231,15 @@ def pandas_eval(self, msg: PandasEvalTool) -> str: # Evaluate the last line and get the result; # SECURITY MITIGATION: Eval input is sanitized by default to prevent most- # common code injection attack vectors.+ # common code injection attack vectors. Additionally, the globals dict+ # restricts ``__builtins__`` so that even with ``full_eval=True`` the+ # expression cannot reach ``__import__``/``eval``/``exec`` via Python's+ # implicit builtin injection (GHSA-q9p7-wqxg-mrhc). try: if not self.config.full_eval: exprn = sanitize_command(exprn) code = compile(exprn, "<calc>", "eval")- eval_result = eval(code, vars, {})+ eval_result = eval(code, safe_eval_globals(vars), {}) except Exception as e: eval_result = f"ERROR: {type(e)}: {e}" AI-assisted change b75c9259d7a0d536a9e41830b71ce0566493f7ccc4b5b04ce410a08f3d04ce4b · Fix 0bb68d14681361d646d2495bba1806e1cc59aaa328aed757ef6bb76c88225930