diff --git a/backend/secuscan/plugins.py b/backend/secuscan/plugins.py index 047e4843..436844ce 100644 --- a/backend/secuscan/plugins.py +++ b/backend/secuscan/plugins.py @@ -15,6 +15,7 @@ from .models import PluginMetadata, PluginFieldType from .config import settings from .capabilities import validate_capability_list, ALL_CAPABILITIES +from .validation import sanitize_input # Port specifications: one or more comma-separated port numbers or port ranges. # Valid: "22", "80,443", "1-1000", "22,80,1000-2000" @@ -378,7 +379,7 @@ def _interpolate(self, token: str, inputs: Dict) -> Optional[str]: return None placeholder = "{" + var_name + (f":{default_value}" if default_value else "") + "}" - rendered = rendered.replace(placeholder, str(value)) + rendered = rendered.replace(placeholder, sanitize_input(str(value))) return rendered diff --git a/backend/secuscan/validation.py b/backend/secuscan/validation.py index 73a7e39b..67f683e3 100644 --- a/backend/secuscan/validation.py +++ b/backend/secuscan/validation.py @@ -382,11 +382,15 @@ def sanitize_input(value: str) -> str: Returns: Sanitized value """ - # Remove shell metacharacters and non-printable control characters + # Remove shell metacharacters and non-printable control characters. dangerous_chars = [';', '|', '&', '$', '`', '(', ')', '<', '>', '\n', '\r', "'", '"', '\\', '!', '{', '}', '\t', '\x00'] for char in dangerous_chars: value = value.replace(char, '') - + + # User-controlled placeholders are passed as argv values, not through a + # shell, but leading dashes can still be interpreted as tool options. + value = value.lstrip("-") + return value.strip() diff --git a/testing/backend/unit/test_plugins.py b/testing/backend/unit/test_plugins.py index 45e5e6ac..4fe80b0a 100644 --- a/testing/backend/unit/test_plugins.py +++ b/testing/backend/unit/test_plugins.py @@ -45,6 +45,16 @@ def test_plugin_manager_build_command(setup_test_environment): assert "http://127.0.0.1" in command +def test_plugin_interpolation_sanitizes_user_controlled_values(): + manager = PluginManager("plugins") + + assert manager._interpolate("{templates}", {"templates": "--debug;$(whoami)"}) == "debugwhoami" + assert ( + manager._interpolate("--user-agent={user_agent}", {"user_agent": "--verbose|curl"}) + == "--user-agent=verbosecurl" + ) + + def test_plugin_list_exposes_runtime_capabilities(setup_test_environment, monkeypatch): """Plugin list payload includes consent and availability details.""" manager = PluginManager(settings.plugins_dir)