diff --git a/shopdb/plugins/__init__.py b/shopdb/plugins/__init__.py index b96c664d..02d232b8 100644 --- a/shopdb/plugins/__init__.py +++ b/shopdb/plugins/__init__.py @@ -4,6 +4,7 @@ from pathlib import Path from typing import Dict, List, Optional from flask import Flask import logging +import re from .base import BasePlugin, PluginMeta from .registry import PluginRegistry, PluginState @@ -469,6 +470,17 @@ class PluginManager: def enable_plugin(self, name: str) -> bool: """Enable a disabled plugin. Auto-installs if available but not installed.""" + # Security: Validate plugin name to prevent path traversal + # Only allow alphanumeric, underscore, and hyphen (matches existing plugins) + if not re.match(r'^[a-zA-Z0-9_-]+$', name): + logger.error(f"Invalid plugin name format: {name}") + return False + + # Reject reserved names that could be used for directory traversal + if name in ('.', '..', ''): + logger.error(f"Invalid plugin name: {name}") + return False + # Auto-install if available but not installed (UX: enabling should just work) if not self.registry.is_installed(name): logger.info(f"Plugin {name} not installed, installing first") diff --git a/shopdb/plugins/loader.py b/shopdb/plugins/loader.py index 6264ac7d..df1050fe 100644 --- a/shopdb/plugins/loader.py +++ b/shopdb/plugins/loader.py @@ -84,6 +84,29 @@ class PluginLoader: return self._manifests[name] manifest_path = self.plugins_dir / name / 'manifest.json' + + # Security: Ensure resolved path stays within plugins_dir boundary + # Defense in depth against path traversal, even if name was validated + resolved_manifest = manifest_path.resolve() + resolved_plugins = self.plugins_dir.resolve() + + # Python 3.9+ has is_relative_to, fallback for older versions + try: + if not resolved_manifest.is_relative_to(resolved_plugins): + raise PluginNotFoundError( + f'Plugin path escapes plugins directory: {name}', + plugin_name=name, + ) + except AttributeError: + # Python < 3.9 fallback using relative_to + try: + resolved_manifest.relative_to(resolved_plugins) + except ValueError: + raise PluginNotFoundError( + f'Plugin path escapes plugins directory: {name}', + plugin_name=name, + ) + if not manifest_path.exists(): raise PluginNotFoundError( f'Plugin {name} has no manifest.json at {manifest_path}',