BUG-HUNT: [security] PluginLoader.validate_protocol() instantiates untrusted plugin class without arguments, executing arbitrary __init__ code during validation #7380

Open
opened 2026-04-10 18:36:46 +00:00 by HAL9000 · 1 comment
Owner

Bug Report: [security] PluginLoader.validate_protocol() instantiates untrusted plugin class during validation, executing arbitrary code

Severity Assessment

  • Impact: PluginLoader.validate_protocol() instantiates the untrusted plugin class (instance = klass()) to check if it satisfies a protocol. A malicious or poorly-written plugin could execute arbitrary code in __init__() during the validation step — before the code has been approved for use
  • Likelihood: Medium — affects any plugin loaded from external/community packages. In-house plugins are low risk, but the security model promises that imports are restricted to an allowlist, which is only part of the protection needed
  • Priority: High

Location

  • File: src/cleveragents/infrastructure/plugins/loader.py
  • Function/Class: PluginLoader.validate_protocol()
  • Lines: ~130-165

Description

The PluginLoader.validate_protocol() method is supposed to check if a plugin class satisfies a protocol. The issue is that it instantiates the class to do so:

@staticmethod
def validate_protocol(klass: type[Any], protocol: type[Any]) -> bool:
    # Try instance check first (most reliable for runtime_checkable)
    try:
        instance = klass()  # BUG: Executes __init__() of untrusted class!
        if isinstance(instance, protocol):
            return True
    except Exception:
        # If instantiation fails, try subclass check
        try:
            if issubclass(klass, protocol):
                return True
        except TypeError:
            pass

The module-level allowlist (_DEFAULT_ALLOWED_PREFIXES = ("cleveragents.",)) prevents importing from non-allowed modules. However, once imported, the class can still execute arbitrary code in __init__(). This is especially problematic for plugins loaded via entry points (load_from_entry_points()), which can come from any installed package.

For example, a plugin entry point that registers a class with a malicious __init__() would have that code executed when validate_protocol() is called, even before the plugin is officially "activated".

Additionally, the allowlist check only applies to load_class() (which uses _validate_module_prefix()), but load_from_entry_points() calls ep.load() before checking the module prefix:

for ep in eps:
    try:
        ep.load()  # BUG: Loads and imports the class without prefix check!
        # Only afterward is the module_path extracted for the descriptor

Evidence

@staticmethod
def validate_protocol(klass: type[Any], protocol: type[Any]) -> bool:
    """Check whether *klass* satisfies a ``@runtime_checkable`` Protocol."""
    try:
        instance = klass()  # BUG: Executes __init__ of potentially untrusted class!
        if isinstance(instance, protocol):
            return True
    except Exception:
        ...
def load_from_entry_points(self, group: str = "cleveragents.plugins") -> list[PluginDescriptor]:
    for ep in eps:
        try:
            ep.load()  # BUG: Loads class WITHOUT allowlist prefix check!
            module_path = ep.value.rsplit(":", 1)[0] if ":" in ep.value else ""
            # The allowlist check should happen BEFORE ep.load()!

Expected Behavior

  1. validate_protocol() should use structural type checking (e.g., issubclass() or attribute inspection) rather than instantiating the class
  2. load_from_entry_points() should check the module prefix BEFORE calling ep.load()

Actual Behavior

Plugin class __init__() is executed during validation before the plugin has been authorized for activation. This is an TOCTOU (Time of Check Time of Use) security issue in the plugin loading pipeline.

Suggested Fix

@staticmethod
def validate_protocol(klass: type[Any], protocol: type[Any]) -> bool:
    """Check whether *klass* satisfies a ``@runtime_checkable`` Protocol."""
    # Use structural type checking only - never instantiate untrusted classes
    try:
        if issubclass(klass, protocol):
            return True
    except TypeError:
        pass
    
    msg = f"Class '{klass.__name__}' does not satisfy protocol '{protocol.__name__}'."
    raise ProtocolMismatchError(msg)

Category

security

TDD Note

After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: @tdd_issue, @tdd_issue_, and @tdd_expected_fail to prove the bug exists before fixing it.


Automated by CleverAgents Bot
Supervisor: Bug Detection Pool | Agent: bug-hunt-pool-supervisor

## Bug Report: [security] PluginLoader.validate_protocol() instantiates untrusted plugin class during validation, executing arbitrary code ### Severity Assessment - **Impact**: `PluginLoader.validate_protocol()` instantiates the untrusted plugin class (`instance = klass()`) to check if it satisfies a protocol. A malicious or poorly-written plugin could execute arbitrary code in `__init__()` during the validation step — before the code has been approved for use - **Likelihood**: Medium — affects any plugin loaded from external/community packages. In-house plugins are low risk, but the security model promises that imports are restricted to an allowlist, which is only part of the protection needed - **Priority**: High ### Location - **File**: `src/cleveragents/infrastructure/plugins/loader.py` - **Function/Class**: `PluginLoader.validate_protocol()` - **Lines**: ~130-165 ### Description The `PluginLoader.validate_protocol()` method is supposed to check if a plugin class satisfies a protocol. The issue is that it instantiates the class to do so: ```python @staticmethod def validate_protocol(klass: type[Any], protocol: type[Any]) -> bool: # Try instance check first (most reliable for runtime_checkable) try: instance = klass() # BUG: Executes __init__() of untrusted class! if isinstance(instance, protocol): return True except Exception: # If instantiation fails, try subclass check try: if issubclass(klass, protocol): return True except TypeError: pass ``` The module-level allowlist (`_DEFAULT_ALLOWED_PREFIXES = ("cleveragents.",)`) prevents importing from non-allowed modules. However, once imported, the class can still execute arbitrary code in `__init__()`. This is especially problematic for plugins loaded via entry points (`load_from_entry_points()`), which can come from **any installed package**. For example, a plugin entry point that registers a class with a malicious `__init__()` would have that code executed when `validate_protocol()` is called, even before the plugin is officially "activated". Additionally, the allowlist check only applies to `load_class()` (which uses `_validate_module_prefix()`), but `load_from_entry_points()` calls `ep.load()` **before** checking the module prefix: ```python for ep in eps: try: ep.load() # BUG: Loads and imports the class without prefix check! # Only afterward is the module_path extracted for the descriptor ``` ### Evidence ```python @staticmethod def validate_protocol(klass: type[Any], protocol: type[Any]) -> bool: """Check whether *klass* satisfies a ``@runtime_checkable`` Protocol.""" try: instance = klass() # BUG: Executes __init__ of potentially untrusted class! if isinstance(instance, protocol): return True except Exception: ... ``` ```python def load_from_entry_points(self, group: str = "cleveragents.plugins") -> list[PluginDescriptor]: for ep in eps: try: ep.load() # BUG: Loads class WITHOUT allowlist prefix check! module_path = ep.value.rsplit(":", 1)[0] if ":" in ep.value else "" # The allowlist check should happen BEFORE ep.load()! ``` ### Expected Behavior 1. `validate_protocol()` should use structural type checking (e.g., `issubclass()` or attribute inspection) rather than instantiating the class 2. `load_from_entry_points()` should check the module prefix BEFORE calling `ep.load()` ### Actual Behavior Plugin class `__init__()` is executed during validation before the plugin has been authorized for activation. This is an TOCTOU (Time of Check Time of Use) security issue in the plugin loading pipeline. ### Suggested Fix ```python @staticmethod def validate_protocol(klass: type[Any], protocol: type[Any]) -> bool: """Check whether *klass* satisfies a ``@runtime_checkable`` Protocol.""" # Use structural type checking only - never instantiate untrusted classes try: if issubclass(klass, protocol): return True except TypeError: pass msg = f"Class '{klass.__name__}' does not satisfy protocol '{protocol.__name__}'." raise ProtocolMismatchError(msg) ``` ### Category security ### TDD Note After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: @tdd_issue, @tdd_issue_<this-issue-number>, and @tdd_expected_fail to prove the bug exists before fixing it. --- **Automated by CleverAgents Bot** Supervisor: Bug Detection Pool | Agent: bug-hunt-pool-supervisor
HAL9000 added this to the v3.5.0 milestone 2026-04-11 00:21:05 +00:00
Author
Owner

Issue triaged by project owner:

  • State: Verified
  • Priority: Critical — PluginLoader.validate_protocol() instantiates untrusted plugin before allowlist check. Security bypass.
  • Milestone: v3.5.0 (M6: Autonomy Hardening) — Plugin security is core to M6 guard enforcement
  • Story Points: 3 (M) — Security fix
  • MoSCoW: Must Have — Plugin security must be enforced before any instantiation

Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

Issue triaged by project owner: - **State**: Verified - **Priority**: Critical — PluginLoader.validate_protocol() instantiates untrusted plugin before allowlist check. Security bypass. - **Milestone**: v3.5.0 (M6: Autonomy Hardening) — Plugin security is core to M6 guard enforcement - **Story Points**: 3 (M) — Security fix - **MoSCoW**: Must Have — Plugin security must be enforced before any instantiation --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#7380
No description provided.