Root cause
Injection & unsafe execution introduced
security: Add capability-aware import hooks to block dangerous modules
How AI contributed: Direct introductionPublished Feb 10, 2026 in
jahlives/openssl_encryptAll 20 findings in this repository
openssl_encrypt versions before 1.4.0 contain a logging bug in restore_hidden_modules() that logs module counts after clearing, always showing zero restored modules and corrupting audit trails. Additionally, a race condition exists between module hiding and import hook installation where another thread could re-import blocked modules in multi-threaded environments.
PluginImportGuard.restore_hidden_modules() restored entries, cleared self.hidden_modules, and only then logged len(self.hidden_modules), so every restoration audit event falsely reported zero. The same guard removed blocked modules from sys.modules before inserting itself into sys.meta_path; a concurrent thread could re-import a removed module in that interval, repopulating sys.modules and making the supposedly blocked module reachable during plugin execution.
Root cause
security: Add capability-aware import hooks to block dangerous modules
Fix
security: Fix restore_hidden_modules() logging and thread safety (GHSA-43r4-3hf9-m84q)
AI-assisted fix: Claude Code · Tobi
Case facts
@@ -31,6 +31,116 @@ logger = logging.getLogger(__name__) +class PluginImportGuard:+ """+ Import hook to block dangerous module imports in plugin code.++ Prevents plugins from importing restricted modules that could be used+ to bypass sandbox restrictions (e.g., subprocess, os, socket).++ Installed in sys.meta_path before plugin execution.+ Also temporarily removes dangerous modules from sys.modules to prevent+ access to already-imported modules.+ """++ # Modules that should always be blocked+ ALWAYS_BLOCKED = {+ 'ctypes', # Load arbitrary C code+ '__builtin__', # Access to builtins+ '__builtins__', # Access to builtins+ }++ # Modules that are blocked unless specific capability is granted+ CAPABILITY_GATED = {+ 'subprocess': None, # Always blocked (no capability for this)+ 'os': None, # Always blocked (contains os.system, etc.)+ 'socket': PluginCapability.NETWORK_ACCESS, # Allowed with NETWORK_ACCESS+ }++ def __init__(self, context: PluginSecurityContext = None):+ """Initialize the import guard.++ Args:+ context: Plugin security context with capabilities+ """+ self.context = context+ self.hidden_modules = {}++ def _should_block_module(self, module_name: str) -> bool:+ """Check if a module should be blocked based on capabilities.++ Args:+ module_name: Name of the module to check++ Returns:+ True if module should be blocked, False if allowed+ """+ # Always block certain modules+ if module_name in self.ALWAYS_BLOCKED:+ return True++ # Check capability-gated modules+ if module_name in self.CAPABILITY_GATED:+ required_capability = self.CAPABILITY_GATED[module_name]+ # If no capability can grant access (None), always block+ if required_capability is None:+ return True+ # If capability is granted, allow access+ if self.context and required_capability in self.context.capabilities:+ return False+ # Otherwise, block+ return True++ return False++ def hide_dangerous_modules(self):+ """+ Remove dangerous modules from sys.modules to prevent access.++ Saves removed modules so they can be restored later.+ """+ # Get all modules that should be blocked+ modules_to_hide = set(self.ALWAYS_BLOCKED) | set(self.CAPABILITY_GATED.keys())++ for module_name in modules_to_hide:+ # Check if module should actually be blocked based on capabilities+ if self._should_block_module(module_name) and module_name in sys.modules:+ self.hidden_modules[module_name] = sys.modules[module_name]+ del sys.modules[module_name]+ logger.debug(f"Hidden module from sys.modules: {module_name}")++ def restore_hidden_modules(self):+ """Restore previously hidden modules to sys.modules."""+ for module_name, module in self.hidden_modules.items():+ sys.modules[module_name] = module+ self.hidden_modules.clear()+ logger.debug(f"Restored {len(self.hidden_modules)} hidden modules")++ def find_module(self, fullname, path=None):@@ -97,6 +97,7 @@ def __init__(self, context: PluginSecurityContext = None): """ self.context = context self.hidden_modules = {}+ self._modules_lock = threading.Lock() def _should_block_module(self, module_name: str) -> bool: """Check if a module should be blocked based on capabilities.@@ -130,23 +131,33 @@ def hide_dangerous_modules(self): Remove dangerous modules from sys.modules to prevent access. Saves removed modules so they can be restored later.+ Thread-safe: uses _modules_lock to prevent concurrent modification. """- # Get all modules that should be blocked- modules_to_hide = set(self.ALWAYS_BLOCKED) | set(self.CAPABILITY_GATED.keys())+ with self._modules_lock:+ # Get all modules that should be blocked+ modules_to_hide = set(self.ALWAYS_BLOCKED) | set(self.CAPABILITY_GATED.keys()) - for module_name in modules_to_hide:- # Check if module should actually be blocked based on capabilities- if self._should_block_module(module_name) and module_name in sys.modules:- self.hidden_modules[module_name] = sys.modules[module_name]- del sys.modules[module_name]- logger.debug(f"Hidden module from sys.modules: {module_name}")+ for module_name in modules_to_hide:+ # Check if module should actually be blocked based on capabilities+ if self._should_block_module(module_name) and module_name in sys.modules:+ self.hidden_modules[module_name] = sys.modules[module_name]+ del sys.modules[module_name]+ logger.debug(f"Hidden module from sys.modules: {module_name}") def restore_hidden_modules(self):- """Restore previously hidden modules to sys.modules."""- for module_name, module in self.hidden_modules.items():- sys.modules[module_name] = module- self.hidden_modules.clear()- logger.debug(f"Restored {len(self.hidden_modules)} hidden modules")+ """Restore previously hidden modules to sys.modules.++ Thread-safe: uses _modules_lock to prevent concurrent modification.+ Wrapped in try/finally to ensure modules are restored even on error.+ """+ with self._modules_lock:+ count = len(self.hidden_modules)+ try:+ for module_name, module in self.hidden_modules.items():+ sys.modules[module_name] = module+ finally:+ self.hidden_modules.clear()+ logger.debug(f"Restored {count} hidden modules") def find_module(self, fullname, path=None): """AI-assisted change 2520086ee80b6eab9b564016a8b181fbc50bf75a7e6741fbdcf6aaba9ec61d35 · Fix e49c53790f8f2bcc881fd5ed9599c408808a89686ab05341275cee8bfa53284e