Back to skill

Security audit

Swmm Network

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly does what it claims, but it needs review because untrusted GIS text can be written into SWMM input files without escaping or validation.

Review or harden this skill before using it with third-party or untrusted municipal data. In particular, validate node/link IDs and textual fields before INP export, reject newlines/brackets/comment delimiters, and inspect generated INP files before running simulations. No evidence of malware, persistence, credential theft, or remote code execution was found in the inspected artifact.

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/network_to_inp.py:21
Finding

Untrusted Model Fields Permit SWMM INP Configuration Injection

Content
View full analysis
str: if x is None: return "" if isinstance(x, bool): return "YES" if x else "NO" if isinstance(x, int): return str(x) if isinstance(x, float): return f"{x:.6f}".rstrip("0").rstrip(".") return str(x) def emit_junctions(network: dict) -> list[str]: lines = ["[JUNCTIONS]", ";;Name Elevation MaxDepth InitDepth SurDepth Aponded"] for j in network.get("junctions", []): lines.append( f"{j['id']:<16} {format_num(j['invert_elev']):<14} {format_num(j['max_depth']):<14} {format_num(j.get('init_depth', 0)):<14} {format_num(j.get('sur_depth', 0)):<14} {format_num(j.get('aponded', 0))}" ) return lines def emit_outfalls(network: dict) -> list[str]: lines = ["[OUTFALLS]", ";;Name Elevation Type Stage Data Gated Route To"] for o in network.get("outfalls", []): lines.append( f"{o['id']:<16} {format_num(o['invert_elev']):<14} {o['type']:<14} {format_num(o.get('stage_data', '')):<15} {format_num(o.get('gated', False)):<14} {format_num(o.get('route_to', ''))}" ) return lines def emit_conduits(network: dict) -> list[str]: lines = ["[CONDUITS]", ";;Name From Node To Node Length Roughness InOffset OutOffset InitFlow MaxFlow"] for c in network.get("conduits", ...[truncated 6650 chars]
Remediation
View remediation
Vulnerability Patterns
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (9)

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding

The declared description emphasizes constructing and validating real SWMM pipe-network models from actual pipe inventory data (junctions, conduits, outfalls, xsections, mapping configs, subcatchment wiring) and explicitly says that for data-scarce areas with only limited information, a different tool should be used. The code instead creates a deliberately minimal placeholder network from a single shapefile record with outlet attributes, producing one junction, one outfall, and one conduit. Its own docstring states it is for cases with no pipe-network geometry yet and for smoke runs/fallbacks. That is a materially different primary purpose from the declared full-network builder, so this is a mismatch.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding

The code’s actual purpose is narrower than declared. It is a format-specific importer/transformer for junctions, outfalls, and conduits into JSON using a field-mapping config. It supports GeoJSON and CSV for some layers, and GeoJSON only for conduits. There is no evidence of shapefile reading, SWMM model validation, routing, subcatchment wiring, or broader GIS/CAD ingestion. The description therefore materially overstates the implemented capabilities.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding

The declared description presents a broader pipeline for building, validating, and routing SWMM pipe-network models from raw spatial data sources. This code chunk only performs validation/quality-assurance on an already-prepared JSON network object and produces a report. While validation is one declared aspect, the implemented behavior lacks the key advertised capabilities around ingesting municipal shapefiles or GIS/CAD exports, constructing SWMM models, field mapping, and subcatchment routing. Thus the description overstates and materially misrepresents this code chunk’s actual purpose.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding

The declared description presents a fairly full-featured SWMM network-building toolchain from municipal GIS/CAD exports, including validation, routing, field mapping, and subcatchment linkage. The supplied code is much narrower: it loads an already-structured JSON network object and emits selected SWMM INP sections. There is no evidence of shapefile/GeoJSON/CSV ingestion, CAD export handling, validation routines, routing behavior, or subcatchment processing. While exporting network elements to SWMM INP is related to the general domain, the actual behavior is materially more limited than the declared purpose, so this is a description-behavior mismatch.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding

Without declared permissions the skill's intent is opaque and cannot be validated.

Content

No source excerpt is available for this finding.

Tainted flow: 'outfalls' from pathlib.Path.read_text (line 164, file read) → pathlib.Path.write_text (file write)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · scripts/infer_outfall.py (reported line 166)May include surrounding context.

python
source_crs = pipes_geojson.get("crs")
    outfalls = build_outfalls_geojson(chosen, source_crs)
    out_path.parent.mkdir(parents=True, exist_ok=True)
    out_path.write_text(json.dumps(outfalls, indent=2), encoding="utf-8")

    report = {
        "ok": True,

Tainted flow: 'oriented' from pathlib.Path.read_text (line 213, file read) → pathlib.Path.write_text (file write)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · scripts/reorient_pipes.py (reported line 215)May include surrounding context.

python
oriented, report = reorient(pipes, outfalls, precision=args.coordinate_precision)
    out_path.parent.mkdir(parents=True, exist_ok=True)
    out_path.write_text(json.dumps(oriented, indent=2), encoding="utf-8")
    report["outputs"] = {"pipes_oriented_geojson": str(out_path)}
    report["inputs"] = {
        "pipes_geojson": str(pipes_path),

Tainted flow: 'snapped' from pathlib.Path.read_text (line 207, file read) → pathlib.Path.write_text (file write)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · scripts/snap_pipe_endpoints.py (reported line 209)May include surrounding context.

python
pipes = json.loads(pipes_path.read_text(encoding="utf-8"))
    snapped, report = snap(pipes, args.tolerance_m)
    out_path.parent.mkdir(parents=True, exist_ok=True)
    out_path.write_text(json.dumps(snapped, indent=2), encoding="utf-8")
    report["outputs"] = {"pipes_snapped_geojson": str(out_path)}
    report["inputs"] = {"pipes_geojson": str(pipes_path)}
    print(json.dumps(report, indent=2))

Vague Triggers

Low
Category
Not specified by scanner
Confidence
82% confidence
Finding

This JSON template includes free-form descriptive placeholders such as "" and "<describe: ...>" without constraining accepted values or giving explicit invalid examples. In a manifest-style file, that lack of specificity can lead to inconsistent or unintended interpretations of when and how the template should be used.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.