Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backend/secuscan/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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

Expand Down
8 changes: 6 additions & 2 deletions backend/secuscan/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand Down
10 changes: 10 additions & 0 deletions testing/backend/unit/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading