diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 1959a5253..42048e0e3 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -4,3 +4,8 @@ **Vulnerability:** Found an unused `_attempt_import` function in `src/codeweaver/server/mcp/server.py` that dynamically imports a module directly from unvalidated configuration (`import_module(mw.rsplit(".", 1)[0])`), leading to potential arbitrary code execution. **Learning:** Functions that perform dynamic imports should not be left around in the codebase if they are unused, especially if they are designed to take unvalidated strings as input. **Prevention:** Avoid dynamic imports based on configuration or inputs without strict whitelisting. Use tools like `semgrep` with python security rules to actively catch these patterns. + +## 2026-06-09 - Arbitrary Code Execution via unrestricted ast.Call in eval() +**Vulnerability:** Found a vulnerability in `src/codeweaver/core/di/container.py` where `eval()` was used to evaluate dynamic type strings. While an AST validator checked for safe nodes, it allowed unrestricted `ast.Call` nodes, meaning any function available in the global namespace or builtins could be executed during dependency injection resolution. +**Learning:** Even when using a restricted `eval()` environment with a filtered globals dict and AST validation, permitting generic `ast.Call` execution still allows for severe security holes (Arbitrary Code Execution) if an attacker can manipulate the input strings (type hints). +**Prevention:** Strictly whitelist allowed functions in AST validation (like `Depends`, `Field`, etc.) when evaluating type annotations, and never allow generic `ast.Call` nodes. diff --git a/src/codeweaver/core/di/container.py b/src/codeweaver/core/di/container.py index 7cd68ce98..969693737 100644 --- a/src/codeweaver/core/di/container.py +++ b/src/codeweaver/core/di/container.py @@ -84,7 +84,7 @@ def __init__(self) -> None: self._request_cache: dict[Any, Any] = {} # Keys can be types or callables self._providers_loaded: bool = False # Track if auto-discovery has run - def _safe_eval_type(self, type_str: str, globalns: dict[str, Any]) -> Any | None: + def _safe_eval_type(self, type_str: str, globalns: dict[str, Any]) -> Any | None: # noqa: C901 """Safely evaluate a type string using AST validation. Parses the type string into an AST, validates that it contains only safe @@ -136,6 +136,26 @@ def generic_visit(self, node: ast.AST) -> None: if isinstance(node, ast.Attribute) and node.attr.startswith("__"): raise TypeError(f"Forbidden dunder attribute: {node.attr}") + # Security: Restrict ast.Call to a strict whitelist of known safe functions to + # prevent Arbitrary Code Execution (ACE) during the dynamic type evaluation in `eval()`. + if isinstance(node, ast.Call): + allowed_calls = { + "Depends", + "depends", + "Field", + "PrivateAttr", + "Tag", + "Parameter", + } + func_name = None + if isinstance(node.func, ast.Name): + func_name = node.func.id + elif isinstance(node.func, ast.Attribute): + func_name = node.func.attr + + if func_name not in allowed_calls: + raise TypeError(f"Forbidden call to function: {func_name}") + super().generic_visit(node) try: