diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 1959a5253..dfe446d54 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-04-21 - Arbitrary Code Execution in Type Resolution Eval +**Vulnerability:** Found an arbitrary code execution vulnerability in `src/codeweaver/core/di/container.py` during type resolution. The AST validator `_safe_eval_type` permitted any `ast.Call` nodes to be executed through `eval()` within the provided `globalns` environment. +**Learning:** AST whitelisting must strictly validate the identifier names being called. Validating the node type alone is insufficient to prevent evaluating functions injected via dynamic namespaces. +**Prevention:** Explicitly whitelist permitted identifier names inside `ast.Call` validations to ensure that only expected dependency injection annotations (`Depends`, `depends`, `Field`, `PrivateAttr`) are instantiated. diff --git a/src/codeweaver/core/di/container.py b/src/codeweaver/core/di/container.py index 7cd68ce98..4fc7ede5f 100644 --- a/src/codeweaver/core/di/container.py +++ b/src/codeweaver/core/di/container.py @@ -136,6 +136,14 @@ 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}") + # Restrict function calls to a safe whitelist to prevent arbitrary code execution + if isinstance(node, ast.Call): + allowed_funcs = {"Depends", "depends", "Field", "PrivateAttr"} + if not isinstance(node.func, ast.Name): + raise TypeError(f"Forbidden call: function must be a simple name, got {type(node.func).__name__}") + if node.func.id not in allowed_funcs: + raise TypeError(f"Forbidden call: only {', '.join(allowed_funcs)} are allowed, got {node.func.id}") + super().generic_visit(node) try: