Back to skill

Security audit

Swmm Runner

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its stated SWMM-runner purpose, but its run command can write report/output files outside the intended run directory if given unsafe output names.

Review before installing. Use only trusted .inp files and trusted run parameters, keep run directories inside a disposable workspace, and avoid user-supplied rptName/outName values until the package validates that generated files stay inside the run directory.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (1)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/swmm_runner.py:279
Finding

Unvalidated Output Names Permit Path Traversal and Arbitrary File Overwrite

Content
View full analysis

Vulnerability Details

File Location: scripts/swmm_runner.py, lines 275–289 and 380–381
Vulnerability Type: Path traversal and unconstrained filesystem write
Risk Level: Medium

Vulnerable Code

python
def cmd_run(args):
    inp = args.inp.resolve()
    run_dir = args.run_dir.resolve()
    run_dir.mkdir(parents=True, exist_ok=True)

    rpt = run_dir / (args.rpt_name or "model.rpt")
    out = run_dir / (args.out_name or "model.out")
    stdout_path = run_dir / "stdout.txt"
    stderr_path = run_dir / "stderr.txt"

    timeout = getattr(args, "timeout", DEFAULT_SWMM_TIMEOUT_S)
    rc = run_swmm(inp, rpt, out, stdout_path, stderr_path, timeout=timeout)

The output names are accepted directly from command-line arguments:

python
ap_run.add_argument('--rpt-name', default=None)
ap_run.add_argument('--out-name', default=None)

The resulting paths are supplied to the SWMM process:

python
p = subprocess.run(
    [resolve_swmm5(), str(inp), str(rpt), str(out)],
    capture_output=True,
    text=True,
    timeout=timeout,
)

Technical Analysis

The values of --rpt-name and --out-name are not restricted to plain filenames. Python's pathlib permits both traversal components and absolute paths:

  • A value such as ../../target.rpt escapes the selected run directory.
  • An absolute value such as /home/user/target.rpt supersedes run_dir entirely.

No canonicalization or containment check verifies that the final report and output paths remain below run_dir. The paths are then passed to swmm5, which may create or overwrite files at those locations using the privileges of the user running the skill.

Although subprocess.run uses an argument list and therefore does not introduce shell-command injection, it does not prevent filesystem path traversal. The vulnerability violates the documented expectation that generated artifacts remain in the standard run directory.

Attack Path

  1. An attacker controls or influences ...[truncated 1388 chars]
Remediation
View remediation

Remediation Suggestions

Require output-name arguments to be plain basenames and verify canonical containment before invoking swmm5.

python
def safe_output_path(run_dir: Path, name: str) -> Path:
    candidate_name = Path(name)

    if candidate_name.is_absolute() or candidate_name.name != name:
        raise ValueError("Output name must be a plain filename")

    base = run_dir.resolve()
    candidate = (base / candidate_name).resolve()

    if candidate.parent != base:
        raise ValueError("Output path must remain inside the run directory")

    return candidate

Apply the validation to both destinations:

python
rpt = safe_output_path(run_dir, args.rpt_name or "model.rpt")
out = safe_output_path(run_dir, args.out_name or "model.out")

Additional hardening should include:

  1. Reject names containing directory separators, . or .. path components, and platform-specific alternate separators.
  2. Restrict expected extensions to .rpt and .out where compatibility permits.
  3. Check for symlinks at destination paths before execution so an existing link cannot redirect writes outside run_dir.
  4. Use a dedicated, newly created run directory with restrictive permissions.
  5. Reject collisions with existing files unless overwrite behavior is explicitly requested.
  6. Apply the same validation at the MCP or API boundary so unsafe paths are rejected before reaching the script.
  7. Add regression tests covering ../, absolute Unix paths, Windows drive paths, alternate separators, nested names, and symlink destinations.
Vulnerability Patterns
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (3)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding

The skill describes capabilities that imply shell execution and filesystem read/write access (swmm5 CLI execution, creation of run directories, and report parsing) but does not declare an explicit tool scope such as permissions or allowed-tools. Without capability scoping, an agent or runtime may grant broader-than-intended access, increasing the risk of command execution on attacker-controlled paths or unintended file access when processing untrusted .inp/.rpt locations.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/swmm_runner.py (reported line 129)May include surrounding context.

python
timeout: float = DEFAULT_SWMM_TIMEOUT_S,
) -> int:
    try:
        p = subprocess.run(
            [resolve_swmm5(), str(inp), str(rpt), str(out)],
            capture_output=True,
            text=True,

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/swmm_runner.py (reported line 255)May include surrounding context.

python
def get_swmm5_version() -> str | None:
    try:
        p = subprocess.run([resolve_swmm5(), "--version"], capture_output=True, text=True)
        # swmm5 may not support --version; fall back to parsing help output
        txt = (p.stdout + "\n" + p.stderr).strip()
        m = re.search(r"(\d+\.\d+\.\d+)", txt)

Static analysis

No suspicious patterns detected.