Skip to content

🔒 Fix dangerous eval() in DI container resolution#229

Open
bashandbone wants to merge 3 commits intomainfrom
fix-eval-vulnerability-di-container-15906456217921985240
Open

🔒 Fix dangerous eval() in DI container resolution#229
bashandbone wants to merge 3 commits intomainfrom
fix-eval-vulnerability-di-container-15906456217921985240

Conversation

@bashandbone
Copy link
Contributor

@bashandbone bashandbone commented Mar 16, 2026

Fixed a security vulnerability in the Dependency Injection container where eval() was used for string type resolution. The fix replaces eval() with a more secure implementation that:

  1. Parses the type string into an Abstract Syntax Tree (AST).
  2. Validates that the AST only contains safe nodes (e.g., Names, Attributes, Subscripts, BinOps) required for type hints.
  3. Explicitly blocks access to dunder names and attributes (e.g., __import__).
  4. Executes the evaluation in a restricted environment with __builtins__ disabled.

Added security tests in tests/di/test_container_security.py to verify the fix and ensure that legitimate type expressions (including generics and unions) still resolve correctly.


PR created automatically by Jules for task 15906456217921985240 started by @bashandbone

Summary by Sourcery

Harden DI container string type resolution by replacing unsafe eval usage with AST-validated evaluation and add regression tests for secure and valid type handling.

Bug Fixes:

  • Fix security vulnerability in DI container type resolution by eliminating direct eval of user-controlled type strings.

Enhancements:

  • Introduce an AST-based safe evaluation helper for resolving string type hints while supporting common typing constructs like generics, unions, and Annotated.

Tests:

  • Add security-focused tests to verify safe handling of malicious type strings and correct resolution of valid complex type expressions in the DI container.

Introduces _safe_eval_type to the Container class, which uses AST parsing
and validation to ensure type strings only contain safe constructs.
It blocks dunder access and evaluates in a restricted environment
without builtins, preventing arbitrary code execution while preserving
functionality for complex type hints.

Fixes a security vulnerability in src/codeweaver/core/di/container.py.

Co-authored-by: bashandbone <89049923+bashandbone@users.noreply.github.com>
@google-labs-jules
Copy link
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

Copilot AI review requested due to automatic review settings March 16, 2026 04:06
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Mar 16, 2026

Reviewer's Guide

Replaces insecure use of eval() for DI string type resolution with a restricted AST-validated evaluator and adds security-focused tests to ensure both safe behavior and compatibility with valid type expressions.

Class diagram for updated DI container type resolution

classDiagram
    class Container {
        - _shutdown_hooks: list[Callable]
        - _cleanup_stack: AsyncExitStack | None
        - _request_cache: dict[Any, Any]
        - _providers_loaded: bool
        + _safe_eval_type(type_str: str, globalns: dict[str, Any]) Any
        + _unwrap_annotated(annotation: Any) Any
        + _resolve_string_type(type_str: str, globalns: dict[str, Any]) Any
    }

    Container ..> Container : uses _safe_eval_type in _resolve_string_type
Loading

Flow diagram for safe AST-based type string evaluation

flowchart TD
    A["Start _safe_eval_type"] --> B["Parse type_str with ast.parse(mode=eval)"]
    B -->|SyntaxError| C["Return None"]
    B -->|Parsed successfully| D["Create TypeValidator"]
    D --> E["Visit AST nodes"]

    E --> F{"Node type allowed?"}
    F -->|No| G["Raise ValueError Forbidden AST node"]
    F -->|Yes| H{"Is ast.Name or ast.Attribute?"}
    H -->|Yes and dunder| I["Raise ValueError Forbidden dunder"]
    H -->|No or non-dunder| J["Continue visiting children"]
    J --> E

    I --> K["Exception propagates to caller"]
    G --> K

    E -->|All nodes validated| L["Compile AST to code object"]
    L --> M["Eval code with globals {__builtins__: {}} and local globalns"]
    M --> N["Return evaluated type"]
    C --> O["End _safe_eval_type"]
    N --> O["End _safe_eval_type"]
Loading

File-Level Changes

Change Details Files
Introduce a secure AST-based evaluator for resolving string type hints instead of raw eval().
  • Add a private _safe_eval_type helper on the Container that parses type strings with ast.parse in eval mode.
  • Implement an inner TypeValidator(ast.NodeVisitor) that only permits a constrained set of AST node types relevant to type hints (names, attributes, subscripts, constants, binops, union operator, tuples/lists, calls, keywords, load).
  • Block any access to dunder identifiers by rejecting ast.Name or ast.Attribute nodes starting with '__'.
  • Compile and evaluate the validated AST with eval using a globals dict that has builtins removed and the provided globalns as locals, returning None on syntax error.
src/codeweaver/core/di/container.py
Wire the secure evaluator into existing string type resolution logic.
  • Update _resolve_string_type to call self._safe_eval_type(type_str, globalns) instead of direct eval() for both the initial resolution attempt and the Annotated-enhanced namespace retry.
  • Keep the previous exception suppression behavior so unsafe or invalid type strings fall back to alternative resolution paths or None.
src/codeweaver/core/di/container.py
Add regression and security tests for safe and malicious type string resolution.
  • Create tests/di/test_container_security.py with tests that verify common valid type expressions (simple types, generics, Optional, unions) still resolve correctly through _resolve_string_type.
  • Add a test that Annotated[int, Depends()] is resolved correctly, including origin and args checks via helper get_origin/get_args functions.
  • Add tests ensuring malicious strings (e.g., import, eval, getattr with dunder, accessing class, lambdas) are blocked and result in None.
  • Add tests that explicitly confirm dunder names and attributes (e.g., 'name', 'int.name') are rejected by the resolver.
tests/di/test_container_security.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • The current AST whitelist still allows arbitrary ast.Call + ast.Attribute combinations (e.g., os.system('...')) whenever such names are present in globalns; consider either disallowing Call entirely or restricting it to a small set of explicitly allowed callables (e.g., Depends) to actually prevent remote code execution.
  • Because eval runs with globalns as locals, any powerful objects exposed there can be invoked via allowed nodes (e.g., plain functions or modules); it may be safer to construct a minimal, curated namespace specifically for type resolution rather than passing through the full globals.
  • Right now all failures in _safe_eval_type (syntax errors, forbidden nodes, NameError, etc.) collapse into None at the call site; if distinguishing between 'syntactically invalid' and 'forbidden/unsafe' cases would be useful, you might propagate a specific exception or at least log/annotate the reason before suppressing it.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The current AST whitelist still allows arbitrary `ast.Call` + `ast.Attribute` combinations (e.g., `os.system('...')`) whenever such names are present in `globalns`; consider either disallowing `Call` entirely or restricting it to a small set of explicitly allowed callables (e.g., `Depends`) to actually prevent remote code execution.
- Because `eval` runs with `globalns` as locals, any powerful objects exposed there can be invoked via allowed nodes (e.g., plain functions or modules); it may be safer to construct a minimal, curated namespace specifically for type resolution rather than passing through the full globals.
- Right now all failures in `_safe_eval_type` (syntax errors, forbidden nodes, NameError, etc.) collapse into `None` at the call site; if distinguishing between 'syntactically invalid' and 'forbidden/unsafe' cases would be useful, you might propagate a specific exception or at least log/annotate the reason before suppressing it.

## Individual Comments

### Comment 1
<location path="src/codeweaver/core/di/container.py" line_range="105-114" />
<code_context>
+        class TypeValidator(ast.NodeVisitor):
</code_context>
<issue_to_address>
**🚨 issue (security):** Current validation still allows arbitrary function calls and attribute access from `globalns`, which may undercut the "safe" aspect of `_safe_eval_type` if `type_str` is user-influenced.

Because `ast.Call` and `ast.Attribute` are allowed on anything in `globalns`, a user-controlled `type_str` could call or inspect any object exposed there (e.g., `os.system` or DI helpers), despite `__builtins__` being removed.

To harden this:
- Only allow calls to a small, explicit whitelist (e.g. `Annotated`, `Literal`, `Depends`, `Optional`, `Union`, etc.).
- Restrict attribute access to explicitly approved modules/types (e.g. `typing`, `collections.abc`) or a registry you control.
- Consider banning `ast.Call` entirely unless there is a clearly bounded, vetted set of call targets.

This would better match the “safe eval” intent and avoid arbitrary code paths via type strings.
</issue_to_address>

### Comment 2
<location path="src/codeweaver/core/di/container.py" line_range="112-129" />
<code_context>
+                        ast.Attribute,
+                        ast.Subscript,
+                        ast.Constant,
+                        ast.BinOp,
+                        ast.BitOr,
+                        ast.Load,
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Allowing generic `ast.BinOp` enables arbitrary binary operations in type strings, not just `|` unions.

The validator currently accepts any `ast.BinOp` without checking that the operator is `BitOr`, so expressions like `Foo + Bar` or `Foo * 2` would also be validated and evaluated, which goes beyond what’s needed for type strings.

Consider either:
- Dropping `ast.BinOp` and relying solely on `ast.BitOr`, or
- Keeping `ast.BinOp` but enforcing `isinstance(node.op, ast.BitOr)` and rejecting all other operators.

```suggestion
                if not isinstance(
                    node,
                    (
                        ast.Expression,
                        ast.Name,
                        ast.Attribute,
                        ast.Subscript,
                        ast.Constant,
                        ast.BinOp,
                        ast.BitOr,
                        ast.Load,
                        ast.Tuple,
                        ast.List,
                        ast.Call,
                        ast.keyword,
                    ),
                ):
                    raise ValueError(f"Forbidden AST node in type string: {type(node).__name__}")

                # Restrict binary operations to unions using `|` only
                if isinstance(node, ast.BinOp) and not isinstance(node.op, ast.BitOr):
                    raise ValueError(
                        f"Forbidden binary operator in type string: {type(node.op).__name__}"
                    )
```
</issue_to_address>

### Comment 3
<location path="tests/di/test_container_security.py" line_range="44-46" />
<code_context>
+    assert get_args(resolved_annotated)[0] is int
+    assert isinstance(get_args(resolved_annotated)[1], Depends)
+
+def test_malicious_type_resolution():
+    container = Container()
+    globalns = {"__name__": "__main__"}
+
+    # Malicious strings that should be blocked
+    malicious_strings = [
+        "__import__('os').system('echo VULNERABLE')",
+        "eval('1+1')",
+        "getattr(int, '__name__')",
+        "int.__class__",
+        "(lambda x: x)(1)",
+    ]
+
+    for s in malicious_strings:
+        result = container._resolve_string_type(s, globalns)
+        assert result is None, f"String '{s}' should have been blocked"
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for syntactically invalid or unknown-name type strings to assert they safely return None.

`test_malicious_type_resolution` currently covers semantically dangerous expressions but not syntactically invalid ones (e.g. `"int["`) or unknown names (e.g. `"UnknownType"`). Since `_safe_eval_type` and `_resolve_string_type` both fall back to `None` in those cases, please add a couple of assertions for invalid/unknown strings here or in a separate test to verify we don’t leak exceptions or accidentally resolve them successfully.

```suggestion
    for s in malicious_strings:
        result = container._resolve_string_type(s, globalns)
        assert result is None, f"String '{s}' should have been blocked"

    # Syntactically invalid or unknown type strings should also be safely rejected
    invalid_or_unknown_strings = [
        "int[",         # invalid syntax
        "UnknownType",  # unknown name
    ]

    for s in invalid_or_unknown_strings:
        result = container._resolve_string_type(s, globalns)
        assert result is None, f"Invalid or unknown string '{s}' should resolve to None"
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +105 to +114
class TypeValidator(ast.NodeVisitor):
def generic_visit(self, node: ast.AST) -> None:
# Allowed nodes for type annotations, including support for:
# - Generics: List[int], dict[str, Any] (Subscript, Name, Attribute)
# - Unions: int | str (BinOp, BitOr)
# - Annotated: Annotated[int, Depends(...)] (Call, keyword, Tuple, List)
# - Literals: Literal["foo"] (Constant)
if not isinstance(
node,
(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 issue (security): Current validation still allows arbitrary function calls and attribute access from globalns, which may undercut the "safe" aspect of _safe_eval_type if type_str is user-influenced.

Because ast.Call and ast.Attribute are allowed on anything in globalns, a user-controlled type_str could call or inspect any object exposed there (e.g., os.system or DI helpers), despite __builtins__ being removed.

To harden this:

  • Only allow calls to a small, explicit whitelist (e.g. Annotated, Literal, Depends, Optional, Union, etc.).
  • Restrict attribute access to explicitly approved modules/types (e.g. typing, collections.abc) or a registry you control.
  • Consider banning ast.Call entirely unless there is a clearly bounded, vetted set of call targets.

This would better match the “safe eval” intent and avoid arbitrary code paths via type strings.

Comment on lines +112 to +129
if not isinstance(
node,
(
ast.Expression,
ast.Name,
ast.Attribute,
ast.Subscript,
ast.Constant,
ast.BinOp,
ast.BitOr,
ast.Load,
ast.Tuple,
ast.List,
ast.Call,
ast.keyword,
),
):
raise ValueError(f"Forbidden AST node in type string: {type(node).__name__}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Allowing generic ast.BinOp enables arbitrary binary operations in type strings, not just | unions.

The validator currently accepts any ast.BinOp without checking that the operator is BitOr, so expressions like Foo + Bar or Foo * 2 would also be validated and evaluated, which goes beyond what’s needed for type strings.

Consider either:

  • Dropping ast.BinOp and relying solely on ast.BitOr, or
  • Keeping ast.BinOp but enforcing isinstance(node.op, ast.BitOr) and rejecting all other operators.
Suggested change
if not isinstance(
node,
(
ast.Expression,
ast.Name,
ast.Attribute,
ast.Subscript,
ast.Constant,
ast.BinOp,
ast.BitOr,
ast.Load,
ast.Tuple,
ast.List,
ast.Call,
ast.keyword,
),
):
raise ValueError(f"Forbidden AST node in type string: {type(node).__name__}")
if not isinstance(
node,
(
ast.Expression,
ast.Name,
ast.Attribute,
ast.Subscript,
ast.Constant,
ast.BinOp,
ast.BitOr,
ast.Load,
ast.Tuple,
ast.List,
ast.Call,
ast.keyword,
),
):
raise ValueError(f"Forbidden AST node in type string: {type(node).__name__}")
# Restrict binary operations to unions using `|` only
if isinstance(node, ast.BinOp) and not isinstance(node.op, ast.BitOr):
raise ValueError(
f"Forbidden binary operator in type string: {type(node.op).__name__}"
)

Comment on lines +44 to +46
for s in malicious_strings:
result = container._resolve_string_type(s, globalns)
assert result is None, f"String '{s}' should have been blocked"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add tests for syntactically invalid or unknown-name type strings to assert they safely return None.

test_malicious_type_resolution currently covers semantically dangerous expressions but not syntactically invalid ones (e.g. "int[") or unknown names (e.g. "UnknownType"). Since _safe_eval_type and _resolve_string_type both fall back to None in those cases, please add a couple of assertions for invalid/unknown strings here or in a separate test to verify we don’t leak exceptions or accidentally resolve them successfully.

Suggested change
for s in malicious_strings:
result = container._resolve_string_type(s, globalns)
assert result is None, f"String '{s}' should have been blocked"
for s in malicious_strings:
result = container._resolve_string_type(s, globalns)
assert result is None, f"String '{s}' should have been blocked"
# Syntactically invalid or unknown type strings should also be safely rejected
invalid_or_unknown_strings = [
"int[", # invalid syntax
"UnknownType", # unknown name
]
for s in invalid_or_unknown_strings:
result = container._resolve_string_type(s, globalns)
assert result is None, f"Invalid or unknown string '{s}' should resolve to None"

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Replaces unsafe eval() with AST-validated evaluation in the DI container's string type resolution to prevent code injection.

Changes:

  • Added _safe_eval_type method with AST whitelist validation and dunder blocking
  • Replaced both eval() call sites in _resolve_string_type with _safe_eval_type
  • Added security tests for safe type resolution, malicious inputs, and dunder blocking

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/codeweaver/core/di/container.py New _safe_eval_type method replacing raw eval() calls
tests/di/test_container_security.py Security tests for the new safe evaluation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


# Annotated with Depends
resolved_annotated = container._resolve_string_type("Annotated[int, Depends()]", globalns)
assert get_origin(resolved_annotated) is Annotated
Comment on lines +95 to +143
The evaluated type object.

Raises:
ValueError: If the type string contains forbidden constructs.
"""
try:
tree = ast.parse(type_str, mode="eval")
except SyntaxError:
return None

class TypeValidator(ast.NodeVisitor):
def generic_visit(self, node: ast.AST) -> None:
# Allowed nodes for type annotations, including support for:
# - Generics: List[int], dict[str, Any] (Subscript, Name, Attribute)
# - Unions: int | str (BinOp, BitOr)
# - Annotated: Annotated[int, Depends(...)] (Call, keyword, Tuple, List)
# - Literals: Literal["foo"] (Constant)
if not isinstance(
node,
(
ast.Expression,
ast.Name,
ast.Attribute,
ast.Subscript,
ast.Constant,
ast.BinOp,
ast.BitOr,
ast.Load,
ast.Tuple,
ast.List,
ast.Call,
ast.keyword,
),
):
raise ValueError(f"Forbidden AST node in type string: {type(node).__name__}")

# Block dunder access to prevent escaping the restricted environment
if isinstance(node, ast.Name) and node.id.startswith("__"):
raise ValueError(f"Forbidden dunder name: {node.id}")
if isinstance(node, ast.Attribute) and node.attr.startswith("__"):
raise ValueError(f"Forbidden dunder attribute: {node.attr}")

super().generic_visit(node)

TypeValidator().visit(tree)

# Restricted eval: no builtins allowed
code = compile(tree, "<string>", "eval")
return eval(code, {"__builtins__": {}}, globalns)
Comment on lines +139 to +143
TypeValidator().visit(tree)

# Restricted eval: no builtins allowed
code = compile(tree, "<string>", "eval")
return eval(code, {"__builtins__": {}}, globalns)
@github-actions
Copy link
Contributor

Code Review

I've reviewed this PR which addresses a security vulnerability by replacing eval() with AST-validated type resolution. This is an important security fix. However, I've identified several critical security concerns that need to be addressed before this can be merged.

Critical Security Issues

1. Arbitrary Function Calls Still Allowed 🚨

The current AST whitelist permits ast.Call nodes without restriction. This means if globalns contains callable objects like os.system, open, or any function, they can still be invoked through the "safe" eval:

# If globalns contains os module, this would still work:
"os.system('malicious command')"

Recommendation: Either:

  • Remove ast.Call entirely from the whitelist if not needed for core type resolution
  • OR restrict calls to an explicit allowlist of safe typing constructors (e.g., Annotated, Depends, Literal, Optional, Union)

See: src/codeweaver/core/di/container.py:125

2. Unrestricted Binary Operations

The validator allows all ast.BinOp operations, not just | (union). This permits expressions like:

"Foo + Bar"  # Addition
"Foo * 2"    # Multiplication  
"Foo << 8"   # Bit shifts

Recommendation: Add a check to ensure only ast.BitOr is allowed:

if isinstance(node, ast.BinOp) and not isinstance(node.op, ast.BitOr):
    raise ValueError(f"Forbidden binary operator: {type(node.op).__name__}")

See: src/codeweaver/core/di/container.py:120

3. globalns Namespace Exposure

The implementation passes the entire globalns dict to eval as locals. This means ANY objects in that namespace can be accessed via attribute chains or calls. If globalns ever contains modules, file handles, or other powerful objects, they become attack vectors.

Recommendation: Consider curating a minimal, safe namespace specifically for type resolution rather than passing through arbitrary globals.

Code Quality Issues

4. Missing Test: get_origin and get_args Import

The test file defines its own get_origin and get_args helpers (lines 58-66) but should import them from typing instead, matching the pattern used in the main codebase:

from typing import Annotated, List, Optional, Union, get_args, get_origin

This is the standard approach used in src/codeweaver/core/di/container.py:20.

See: tests/di/test_container_security.py:58-66

5. Incomplete Test Coverage

The tests should cover:

  • Syntactically invalid strings: "int["
  • Unknown type names: "UnknownType"
  • Edge cases with nested generics: "Dict[str, List[Optional[int]]]"

Documentation Concerns

6. Outdated Docstring

The docstring in _resolve_string_type (lines 176-184) still explains why eval() is safe, but this is now misleading since we're using _safe_eval_type. The docstring should be updated to explain the AST-based validation approach.

See: src/codeweaver/core/di/container.py:176-184

Positive Aspects ✅

  • Good use of AST validation as a defense-in-depth measure
  • Dunder blocking is effective for preventing __import__ and __class__ access
  • Disabling __builtins__ is a solid baseline protection
  • Security-focused tests are a great addition

Recommendation

Request Changes - The security issues (#1, #2, #3) must be addressed before merging. The arbitrary function call vulnerability undermines the core security goal of this PR.

Once these are fixed, this will be a solid security improvement for the codebase.


Note: Tests are currently failing in CI. Please ensure all tests pass before requesting re-review.

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR addresses an important security vulnerability, but the current implementation has critical security gaps that allow arbitrary code execution through the AST whitelist. Please address the security issues outlined in my detailed review comment before merging.

Key concerns:

  1. Arbitrary function calls still permitted via ast.Call
  2. All binary operations allowed (not just unions)
  3. Full globalns exposure creates attack surface
  4. Missing test coverage for edge cases

The approach is sound, but needs hardening to actually prevent code injection attacks.

@github-actions
Copy link
Contributor

Security Fix Review

Thank you for addressing this critical security vulnerability! The replacement of eval() with AST-validated type resolution is an important improvement. Here's my detailed review:

Strengths

  1. Security Hardening: The new _safe_eval_type() method properly blocks dangerous constructs:

    • Validates AST nodes to only allow safe type annotation constructs
    • Blocks dunder names (__import__, etc.) and attributes (__class__, etc.)
    • Disables __builtins__ during evaluation
  2. Comprehensive Test Coverage: The new security tests in tests/di/test_container_security.py cover:

    • Valid type expressions (generics, unions, Annotated, etc.)
    • Malicious code injection attempts
    • Dunder access blocking
  3. Maintained Functionality: The implementation preserves support for complex type hints:

    • Generics: List[int], dict[str, Any]
    • Union types: int | str
    • Annotated types: Annotated[int, Depends()]
    • Literals: Literal["foo"]

⚠️ Issues to Address

1. Missing ast.Store and ast.Del contexts

The AST validator allows ast.Load but may encounter ast.Store or ast.Del contexts in edge cases. While unlikely in type expressions, the validator should explicitly handle or reject these for completeness.

Location: src/codeweaver/core/di/container.py:112-127

2. Incomplete malicious string test coverage

The test case "getattr(int, '__name__')" in test_malicious_type_resolution() should actually be blocked, but it may not be because:

  • ast.Call is in the allowed nodes list (needed for Annotated[int, Depends()])
  • However, calling getattr is dangerous

Recommendation: Add validation to block function calls except for known safe constructors (like Depends, type constructors, etc.)

Location: tests/di/test_container_security.py:35-46

3. Error handling could be more informative

When _safe_eval_type() catches a ValueError from the AST validator, it's silently suppressed by the with suppress(Exception) in _resolve_string_type(). This makes debugging legitimate type resolution failures difficult.

Suggestion: Consider logging validation failures at DEBUG level before returning None.

Location: src/codeweaver/core/di/container.py:197-198

4. Test failures in CI

The PR shows test failures in Python 3.12, 3.13, 3.14, and 3.14t. We need to understand what's failing before approving. The failures may indicate:

  • Breaking changes to existing type resolution behavior
  • Missing imports or dependencies in tests
  • Edge cases not covered by the new implementation

Action needed: Please investigate and share the test failure details.

📝 Suggested Improvements

Strengthen AST validation:

# In _safe_eval_type, add more specific Call validation
if isinstance(node, ast.Call):
    # Only allow calls to known-safe type constructors
    if isinstance(node.func, ast.Name):
        allowed_callables = {'Depends', 'Annotated'}  # Extend as needed
        if node.func.id not in allowed_callables and node.func.id not in globalns:
            raise ValueError(f"Forbidden function call: {node.func.id}")

Add debug logging:

def _safe_eval_type(self, type_str: str, globalns: dict[str, Any]) -> Any:
    try:
        tree = ast.parse(type_str, mode="eval")
    except SyntaxError as e:
        logger.debug(f"Failed to parse type string '{type_str}': {e}")
        return None
    
    try:
        TypeValidator().visit(tree)
    except ValueError as e:
        logger.debug(f"Type string '{type_str}' failed validation: {e}")
        raise  # Re-raise to be caught by outer handler

🔍 Questions

  1. Have you tested this with real-world Annotated type hints that include complex Depends() expressions?
  2. What are the specific test failures shown in CI? Can you share the error messages?
  3. Should we consider adding a whitelist of allowed callable names instead of allowing all ast.Call nodes?

⏸️ Recommendation

Request Changes - While the security fix is excellent and well-implemented, the CI test failures must be resolved before merging. Please:

  1. Investigate and fix the failing tests in Python 3.12-3.14
  2. Consider strengthening the ast.Call validation as suggested above
  3. Add debug logging for better troubleshooting

Once these issues are addressed, this will be a valuable security improvement. Great work on identifying and fixing this vulnerability! 🔒

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security Fix Review

I've reviewed PR #229 which addresses a critical security vulnerability in the DI container's type resolution. Here's my comprehensive analysis:

✅ Security Fix Quality

The replacement of eval() with AST-validated type resolution is excellent and follows security best practices:

  1. AST Validation: The _safe_eval_type method properly validates the AST before evaluation
  2. Allowlist Approach: Only permits specific AST node types needed for type hints (Name, Attribute, Subscript, BinOp, etc.)
  3. Dunder Blocking: Explicitly blocks dunder names/attributes to prevent __import__, __builtins__, etc.
  4. Restricted Environment: Evaluates with {"__builtins__": {}} to disable built-in functions
  5. Graceful Failure: Returns None on syntax errors or validation failures

✅ Test Coverage

The security tests in test_container_security.py are well-designed:

  • ✅ Tests valid type expressions (generics, unions, Annotated)
  • ✅ Tests malicious injection attempts
  • ✅ Tests dunder attribute/name blocking

❌ CI Failure - Missing License Header

The tests are failing due to a missing REUSE license header in the new test file. The file needs this header at the top:

# SPDX-FileCopyrightText: 2025 Knitli Inc.
# SPDX-FileContributor: Adam Poulemanos <adam@knit.li>
#
# SPDX-License-Identifier: MIT OR Apache-2.0

This is required for REUSE compliance (all project files need copyright/license info).

🔍 Code Quality Observations

Strengths:

  • Clear, comprehensive documentation in docstrings
  • Proper error handling with try/except for SyntaxError
  • Maintains backward compatibility with existing type resolution
  • Uses contextlib.suppress appropriately for fallback behavior

Minor Suggestions:

  1. Potential Issue with eval() Exception Handling: In _safe_eval_type line 143, the eval() call could raise exceptions beyond ValueError. Consider wrapping it:

    try:
        return eval(code, {"__builtins__": {}}, globalns)
    except Exception:
        return None
  2. AST Node Coverage: Consider if you need to support:

    • ast.Slice for slice notation in generics
    • ast.Index (deprecated in 3.9+ but may exist in AST for compatibility)
  3. Test Enhancement: Add a test case for deeply nested generics like Dict[str, List[Optional[int]]] to ensure the validator handles complex nesting.

📝 Recommendations

Required for merge:

  • Add the REUSE license header to tests/di/test_container_security.py

Suggested improvements (optional):

  • Add broader exception handling around the eval() call
  • Consider adding more edge case tests for complex nested types
  • Document the security rationale in the module docstring

🎯 Overall Assessment

This is a high-quality security fix that properly addresses the eval() vulnerability. The AST-based approach is industry-standard for safe dynamic type evaluation. Once the license header is added, this should be ready to merge.

The fix appropriately balances security with functionality - it blocks malicious code while still supporting legitimate type hint patterns including generics, unions, and Annotated types with Depends().


Verdict: Approve pending license header fix ✅

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security Review of PR #229

I've completed a thorough security review of this PR that fixes a critical security vulnerability in the DI container. Here's my assessment:

Excellent Security Fix

The PR successfully replaces the dangerous eval() call with a robust AST-based validation approach. The implementation is well-designed and comprehensive.

Strengths

  1. Strong AST Validation (container.py:87-143)

    • Uses ast.parse() to create an Abstract Syntax Tree before evaluation
    • Explicitly whitelists only safe node types needed for type annotations
    • Supports complex typing constructs: Generics, Unions, Annotated, Literals, etc.
    • Clean implementation with clear comments explaining what each node type is for
  2. Dunder Protection (container.py:132-135)

    • Blocks __import__, __builtins__, and other dunder access
    • Prevents escaping the restricted environment
    • Covers both ast.Name and ast.Attribute node types
  3. Restricted Execution Environment (container.py:141-143)

    • Sets __builtins__: {} to prevent access to built-in functions
    • Limits scope to only the provided globalns
    • Multiple layers of defense
  4. Comprehensive Test Coverage (tests/di/test_container_security.py)

    • Tests valid type resolution including complex generics
    • Tests malicious string blocking (eval, import, getattr, lambda, etc.)
    • Tests dunder blocking specifically
    • Good variety of attack vectors covered
  5. Graceful Error Handling (container.py:100-103)

    • Returns None on syntax errors instead of crashing
    • Maintains backward compatibility

Issues Identified

🔴 Critical: Missing License Headers

The new test file tests/di/test_container_security.py is missing the required SPDX headers, causing CI failures. The file needs these headers at the top:

# SPDX-FileCopyrightText: 2025 Knitli Inc.
# SPDX-FileContributor: Adam Poulemanos <adam@knit.li>
#
# SPDX-License-Identifier: MIT OR Apache-2.0

This is the root cause of all test failures.

⚠️ Minor: Import Organization

In the test file (test_container_security.py:2-5), imports should be organized:

  • Standard library imports first
  • Third-party imports (pytest) next
  • Local imports last

Should be:

from typing import Annotated, List, Optional, Union

import pytest

from codeweaver.core.di.container import Container
from codeweaver.core.di.dependency import Depends

📝 Minor: Test Documentation

The test file would benefit from a module docstring explaining the security concerns being tested, similar to other test files in the project (see test_provider_api.py:5-17).

Edge Cases to Consider

The implementation handles these well, but worth noting:

  1. Complex Nested Types: The code correctly handles deeply nested generics like Dict[str, List[Tuple[int, str]]]
  2. Union Syntax: Both old-style Union[X, Y] and new-style X | Y are supported via ast.BinOp and ast.BitOr
  3. Syntax Errors: Gracefully returns None rather than crashing
  4. Performance: AST parsing adds minimal overhead compared to the security benefit

Recommendations

  1. Fix the license headers - This will make all tests pass
  2. Consider adding a test for deeply nested complex types to document this works:
    assert container._resolve_string_type("Dict[str, List[Tuple[int, Optional[str]]]]", globalns)
  3. Consider documenting in the code comment that this approach is resistant to:
    • Code injection via __import__
    • Attribute access exploits via __builtins__
    • Lambda/function definition attacks
    • Class instantiation attacks

Security Assessment

The security fix is sound and ready to merge once the license header issue is resolved.

The multi-layered defense approach (AST validation + dunder blocking + restricted environment) provides excellent protection against arbitrary code execution while maintaining full functionality for legitimate type resolution.

Approval Status

I'm marking this as requesting changes solely due to the missing license headers causing CI failures. Once those are added, this is ready to merge.

Great work on identifying and fixing this critical vulnerability! 🔒

@bashandbone
Copy link
Contributor Author

bashandbone commented Mar 16, 2026

@jules please rebase and address the following PR comments:

Issues Identified

🔴 Critical: Missing License Headers
The new test file tests/di/test_container_security.py is missing the required SPDX headers, causing CI failures. The file needs these headers at the top:

# SPDX-FileCopyrightText: 2025 Knitli Inc.
# SPDX-FileContributor: Adam Poulemanos <adam@knit.li>
#
# SPDX-License-Identifier: MIT OR Apache-2.0

This is the root cause of all test failures.

⚠️ Minor: Import Organization
In the test file (test_container_security.py:2-5), imports should be organized:

Standard library imports first
Third-party imports (pytest) next
Local imports last
Should be:

from typing import Annotated, List, Optional, Union

import pytest

from codeweaver.core.di.container import Container
from codeweaver.core.di.dependency import Depends

📝 Minor: Test Documentation
The test file would benefit from a module docstring explaining the security concerns being tested, similar to other test files in the project (see test_provider_api.py:5-17).

Edge Cases to Consider
The implementation handles these well, but worth noting:

Complex Nested Types: The code correctly handles deeply nested generics like Dict[str, List[Tuple[int, str]]]
Union Syntax: Both old-style Union[X, Y] and new-style X | Y are supported via ast.BinOp and ast.BitOr
Syntax Errors: Gracefully returns None rather than crashing
Performance: AST parsing adds minimal overhead compared to the security benefit

@google-labs-jules
Copy link
Contributor

@jules please rebase and address the following PR comments:

Issues Identified
🔴 Critical: Missing License Headers
The new test file tests/di/test_container_security.py is missing the required SPDX headers, causing CI failures. The file needs these headers at the top:

SPDX-FileCopyrightText: 2025 Knitli Inc.

SPDX-FileContributor: Adam Poulemanos adam@knit.li

SPDX-License-Identifier: MIT OR Apache-2.0

This is the root cause of all test failures.

⚠️ Minor: Import Organization
In the test file (test_container_security.py:2-5), imports should be organized:

Standard library imports first
Third-party imports (pytest) next
Local imports last
Should be:

from typing import Annotated, List, Optional, Union

import pytest

from codeweaver.core.di.container import Container
from codeweaver.core.di.dependency import Depends

📝 Minor: Test Documentation
The test file would benefit from a module docstring explaining the security concerns being tested, similar to other test files in the project (see test_provider_api.py:5-17).

Edge Cases to Consider
The implementation handles these well, but worth noting:

Complex Nested Types: The code correctly handles deeply nested generics like Dict[str, List[Tuple[int, str]]]
Union Syntax: Both old-style Union[X, Y] and new-style X | Y are supported via ast.BinOp and ast.BitOr
Syntax Errors: Gracefully returns None rather than crashing
Performance: AST parsing adds minimal overhead compared to the security benefit

I've addressed the feedback by adding the missing SPDX license headers, organizing the imports according to the project style (stdlib, third-party, then local), and adding a module docstring to tests/di/test_container_security.py. I also kept the previously implemented robust check for Annotated types to ensure cross-environment compatibility. I'm now submitting the updated changes.

- Replaces `eval()` in `src/codeweaver/core/di/container.py` with `_safe_eval_type`.
- `_safe_eval_type` uses AST validation to allow only type-safe constructs.
- Blocks access to dunder attributes and names to prevent sandbox escapes.
- Evaluates in a restricted environment with a whitelist of safe built-ins (int, str, list, etc.).
- Includes comprehensive security tests in `tests/di/test_container_security.py` with proper license headers and documentation.

Co-authored-by: bashandbone <89049923+bashandbone@users.noreply.github.com>

from typing import Annotated, List, Optional, Union, get_args, get_origin

import pytest
@github-actions
Copy link
Contributor

Security Review: Safe Type Resolution in DI Container

I've reviewed PR #229 and the security fix looks excellent. This is a critical security improvement that properly addresses the eval() vulnerability while maintaining full functionality.

✅ Security Analysis

Vulnerability Fixed:
The PR successfully eliminates the dangerous use of eval() for string type resolution in the DI container (lines 210, 213 in the old code), which could have allowed arbitrary code execution if an attacker controlled type strings.

Security Measures Implemented:

  1. AST-based Validation (src/codeweaver/core/di/container.py:87-159):

    • Parses type strings into an Abstract Syntax Tree
    • Whitelist approach: only allows safe AST node types needed for type hints
    • Supports legitimate use cases: generics (List[int]), unions (int | str), Annotated types, etc.
  2. Dunder Attribute Blocking (lines 132-135):

    • Explicitly blocks access to dunder names (__import__, __name__, etc.)
    • Prevents attribute access like int.__class__ that could be used for sandbox escape
    • This is critical as it prevents common Python introspection-based attacks
  3. Restricted Evaluation Environment (lines 143-159):

    • Uses a minimal whitelist of safe builtins (int, str, list, dict, etc.)
    • Replaces __builtins__ with restricted set, preventing access to dangerous functions
    • Compiled AST is evaluated in controlled namespace

✅ Code Quality

Strengths:

  • Comprehensive test coverage in tests/di/test_container_security.py:
    • ✅ Validates legitimate type expressions work correctly
    • ✅ Tests blocking of malicious patterns (__import__, eval, getattr, lambda, etc.)
    • ✅ Verifies dunder blocking
    • ✅ Confirms safe builtins resolution
  • Clean, well-documented code with clear comments explaining each security measure
  • Proper error handling: returns None on syntax errors or forbidden constructs
  • Maintains backward compatibility with existing functionality

Implementation Details:

  • The fix is applied consistently across all call sites (lines 214, 241, 979, 1015, 1072)
  • Uses contextlib.suppress appropriately for graceful fallback behavior
  • Integration with existing Annotated type resolution logic preserved

⚠️ Minor Observations

  1. Test Failures are Unrelated: The CI failures (KeyError: 'max_file_size', provider resolution errors) are pre-existing issues unrelated to this security fix. The security tests themselves all pass:

    • test_safe_type_resolution - PASSED
    • test_malicious_type_resolution - PASSED
    • test_dunder_blocking - PASSED
    • test_safe_builtins_resolution - PASSED
  2. Safe Builtins List: The whitelist of safe builtins (lines 143-156) is appropriate for type resolution. Consider if None should also be included, though it's likely handled by the global namespace.

🎯 Recommendation

APPROVE

This PR should be merged. It:

  • Fixes a legitimate security vulnerability (CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code)
  • Uses industry best practices (AST validation, whitelist-based security)
  • Maintains functionality for all legitimate use cases
  • Includes comprehensive test coverage
  • Has clear documentation

The failing tests are unrelated infrastructure issues that should be addressed separately.

Security Impact: HIGH - Prevents arbitrary code execution
Code Quality: EXCELLENT - Clean implementation with good tests
Risk: LOW - Well-tested, maintains backward compatibility

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Code Quality Note

Regarding the unused import comment on tests/di/test_container_security.py:14:

Confirmed: The get_origin import is indeed unused. While this is a minor linting issue that doesn't affect the security fix or functionality, it would be good to clean up.

Suggested fix for line 14:

from typing import Annotated, List, Optional, Union, get_args

The import became vestigial because the test (lines 42-48) uses __metadata__ attribute checking instead of get_origin() for cross-version compatibility.

This can be addressed in a follow-up commit if desired, but should not block merging this important security fix.

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 16, 2026 14:01
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Replaces an unsafe eval() call in the DI container's string type resolution with an AST-validated evaluation that restricts allowed node types and blocks dunder access.

Changes:

  • Added _safe_eval_type method with AST validation and restricted builtins to replace raw eval()
  • Updated both call sites in _resolve_string_type to use the new safe method
  • Added security tests verifying malicious strings are blocked and valid type expressions still resolve

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/codeweaver/core/di/container.py Adds _safe_eval_type with AST validation and restricted eval; replaces two eval() calls
tests/di/test_container_security.py New test file covering safe resolution, malicious strings, dunder blocking, and builtin types

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +21

# SPDX-FileCopyrightText: 2025 Knitli Inc.
# SPDX-FileContributor: Adam Poulemanos <adam@knit.li>
#
# SPDX-License-Identifier: MIT OR Apache-2.0

"""Security tests for the dependency injection container.

This module verifies that the DI container safely resolves string type
annotations, preventing arbitrary code execution while supporting
complex Python type hints including generics, unions, and Annotated types.
"""

from typing import Annotated, List, Optional, Union, get_args

import pytest

from codeweaver.core.di.container import Container
from codeweaver.core.di.dependency import Depends

def test_safe_type_resolution():
"dict": dict,
"set": set,
"frozenset": frozenset,
"type": type,
Comment on lines +97 to +103
Raises:
ValueError: If the type string contains forbidden constructs.
"""
try:
tree = ast.parse(type_str, mode="eval")
except SyntaxError:
return None
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants