Skip to content

Commit d709f6f

Browse files
author
Vansh
committed
Fix plugin ID traversal vulnerability and add unit tests
1 parent fd1297e commit d709f6f

2 files changed

Lines changed: 93 additions & 6 deletions

File tree

backend/secuscan/scaffold.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def generate_scaffold(
2929
print("=== SecuScan Plugin Scaffolding ===")
3030

3131
# 1. Interactive Inputs & Fallbacks
32-
if not plugin_id:
32+
if plugin_id is None:
3333
while True:
3434
plugin_id = prompt_input("Plugin ID (e.g. my_scanner): ").lower()
3535
if not plugin_id:
@@ -39,6 +39,14 @@ def generate_scaffold(
3939
print("[-] Plugin ID must contain only lowercase letters, numbers, underscores, or hyphens.")
4040
continue
4141
break
42+
else:
43+
plugin_id = plugin_id.lower()
44+
if not plugin_id:
45+
print("[-] Error: Plugin ID cannot be empty.")
46+
sys.exit(1)
47+
if not re.match(r"^[a-z0-9_-]+$", plugin_id):
48+
print("[-] Error: Plugin ID must contain only lowercase letters, numbers, underscores, or hyphens.")
49+
sys.exit(1)
4250

4351
if not name:
4452
name = prompt_input(f"Plugin Display Name [{plugin_id}]: ", default=plugin_id)
@@ -56,9 +64,18 @@ def generate_scaffold(
5664
print(f"[-] Error: Invalid safety level '{safety}'. Must be 'safe', 'intrusive', or 'exploit'.")
5765
sys.exit(1)
5866

59-
# 2. Check Existing Plugin
60-
plugins_dir = Path(settings.plugins_dir)
61-
target_dir = plugins_dir / plugin_id
67+
# 2. Check Existing Plugin & Enforce Path Containment
68+
plugins_dir = Path(settings.plugins_dir).resolve()
69+
target_dir = (plugins_dir / plugin_id).resolve()
70+
71+
try:
72+
target_dir.relative_to(plugins_dir)
73+
if target_dir == plugins_dir:
74+
raise ValueError()
75+
except (ValueError, RuntimeError):
76+
print(f"[-] Error: Target directory must be inside {plugins_dir}")
77+
sys.exit(1)
78+
6279
if target_dir.exists():
6380
print(f"[-] Error: Plugin directory already exists at: {target_dir}")
6481
sys.exit(1)
@@ -154,8 +171,15 @@ def parse_output(output: str) -> Dict[str, Any]:
154171

155172
# 4. Write Files to Disk (Temporary without checksum)
156173
target_dir.mkdir(parents=True, exist_ok=True)
157-
metadata_file = target_dir / "metadata.json"
158-
parser_file = target_dir / "parser.py"
174+
metadata_file = (target_dir / "metadata.json").resolve()
175+
parser_file = (target_dir / "parser.py").resolve()
176+
177+
try:
178+
metadata_file.relative_to(target_dir)
179+
parser_file.relative_to(target_dir)
180+
except (ValueError, RuntimeError):
181+
print("[-] Error: Write target escaped containment.")
182+
sys.exit(1)
159183

160184
# Write initial files with LF line endings to avoid line ending/checksum issues
161185
metadata_file.write_text(json.dumps(metadata_template, indent=2), encoding="utf-8", newline="\n")
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import os
2+
import shutil
3+
import pytest
4+
from unittest.mock import patch
5+
from pathlib import Path
6+
from backend.secuscan.config import settings
7+
from backend.secuscan.scaffold import generate_scaffold
8+
from scripts.validate_plugin import validate_plugin
9+
10+
@pytest.fixture
11+
def clean_plugins_dir(tmp_path):
12+
"""Temporary plugins directory to isolate scaffolding tests."""
13+
original_plugins_dir = settings.plugins_dir
14+
settings.plugins_dir = str(tmp_path)
15+
yield tmp_path
16+
settings.plugins_dir = original_plugins_dir
17+
18+
def test_scaffold_success(clean_plugins_dir):
19+
"""Test successful plugin scaffolding with valid inputs."""
20+
plugin_id = "test_happy_scanner"
21+
generate_scaffold(plugin_id=plugin_id, name="Test Happy Scanner", safety="safe")
22+
23+
target_dir = clean_plugins_dir / plugin_id
24+
assert target_dir.exists()
25+
assert (target_dir / "metadata.json").exists()
26+
assert (target_dir / "parser.py").exists()
27+
28+
# Validate that it passes validate_plugin script requirements
29+
assert validate_plugin(target_dir) is True
30+
31+
def test_scaffold_invalid_id_traversal(clean_plugins_dir):
32+
"""Test that path traversal attempts in plugin ID are rejected."""
33+
with pytest.raises(SystemExit) as exc:
34+
generate_scaffold(plugin_id="../../outside_scanner", name="Traversal", safety="safe")
35+
assert exc.value.code == 1
36+
37+
def test_scaffold_invalid_id_chars(clean_plugins_dir):
38+
"""Test that invalid characters in plugin ID are rejected."""
39+
with pytest.raises(SystemExit) as exc:
40+
generate_scaffold(plugin_id="Invalid@Scanner", name="Invalid", safety="safe")
41+
assert exc.value.code == 1
42+
43+
def test_scaffold_empty_id(clean_plugins_dir):
44+
"""Test that empty plugin ID is rejected when supplied via CLI args."""
45+
with pytest.raises(SystemExit) as exc:
46+
generate_scaffold(plugin_id="", name="Empty", safety="safe")
47+
assert exc.value.code == 1
48+
49+
def test_scaffold_existing_directory(clean_plugins_dir):
50+
"""Test that scaffolding fails if the target directory already exists."""
51+
plugin_id = "duplicate_scanner"
52+
target_dir = clean_plugins_dir / plugin_id
53+
target_dir.mkdir()
54+
55+
with pytest.raises(SystemExit) as exc:
56+
generate_scaffold(plugin_id=plugin_id, name="Duplicate", safety="safe")
57+
assert exc.value.code == 1
58+
59+
def test_scaffold_invalid_safety_value(clean_plugins_dir):
60+
"""Test that invalid safety levels are rejected."""
61+
with pytest.raises(SystemExit) as exc:
62+
generate_scaffold(plugin_id="valid_id", name="Valid", safety="extreme")
63+
assert exc.value.code == 1

0 commit comments

Comments
 (0)