Back to skill

Security audit

Router9

Security checks for vulnerabilities and agentic risk

Overview

Router9 is a coherent cloud AI and storage skill, but its file transfer commands give the remote service too much control over local reads and writes without enough safeguards.

Review this skill before installing. Use it only if you are comfortable sending selected text, audio, images, and files to Router9-managed services. Avoid uploading secrets or regulated data, pass an explicit safe -o path for downloads, and be careful with remote delete operations because the CLI does not add a confirmation step.

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

Error
Location
scripts/router9.py:201
Finding

Server-Controlled Download Filename Allows Arbitrary File Overwrite

Content
View full analysis

Vulnerability Details

File Location: scripts/router9.py, lines 201–205
Vulnerability Type: Unvalidated server-controlled file path
Risk Level: High

python
download_url = result["data"]["url"]
filename = result["data"].get("filename", "download")

# Step 2: Download from S3
output = args.output or filename
urllib.request.urlretrieve(download_url, output)

Technical Analysis

When the user does not provide the --output option, the script uses a filename returned by the Router9 API as the local download destination. The value is passed directly to urllib.request.urlretrieve without validating whether it is an absolute path, contains parent-directory components such as .., or resolves outside an intended download directory.

Consequently, a malicious or compromised API response could provide a filename such as ../../.ssh/config or an absolute path. The downloaded content would then be written to that location if it is writable by the process. Merely applying Path.resolve() would not be sufficient; the resolved path must also be checked against a trusted destination directory.

The vulnerability does not elevate the process to operating-system administrator privileges. Its scope is limited to files writable by the user or service account running the skill, but those files may include application source, configuration, credentials, shell initialization files, or other security-sensitive resources.

Attack Path

  1. A malicious or compromised Router9 API response returns crafted download metadata containing an absolute filename or directory-traversal sequence.
  2. Alternatively, attacker-controlled storage metadata could create the same condition if the service preserves unsafe filenames.
  3. A user or agent invokes python scripts/router9.py download <file_id> without explicitly supplying --output.
  4. The script assigns the untrusted API filename to output.
  5. urllib.request.urlretrieve writes the remote content to ...[truncated 974 chars]
Remediation
View remediation

Remediation Suggestions

  1. Never use a remote filename directly as a filesystem path. Reduce it to a safe basename and reject empty or special names:
python
from pathlib import Path

raw_filename = result["data"].get("filename", "download")
safe_filename = Path(raw_filename).name

if safe_filename in {"", ".", ".."}:
    raise ValueError("Invalid download filename")
  1. Place implicit downloads in a designated trusted directory, resolve the final path, and verify containment:
python
download_dir = Path.cwd() / "downloads"
download_dir.mkdir(parents=True, exist_ok=True)
download_root = download_dir.resolve()

destination = (download_root / safe_filename).resolve()
if destination.parent != download_root:
    raise ValueError("Download path escapes the destination directory")
  1. Treat an explicit --output path separately. If arbitrary destinations are intended, clearly document that behavior and require explicit user authorization before overwriting an existing file.

  2. Use exclusive file creation or refuse overwrites by default. Provide a separate --force option when replacement is intentionally requested.

  3. Consider downloading to a securely created temporary file in the destination directory and atomically renaming it after the transfer completes successfully.

  4. Add tests covering absolute paths, ../ traversal, nested traversal, platform-specific path separators, empty filenames, symbolic-link destinations, and attempts to overwrite existing files.

Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (16)

Lp1

High
Category
MCP Least Privilege
Confidence
98% confidence
Finding

This skill makes authenticated outbound network requests to Router9 and presigned storage URLs, yet the capability is reportedly undeclared. Hidden network access is especially sensitive because it can transfer user data and files off-host, making the mismatch a significant permission and transparency issue.

Content

No source excerpt is available for this finding.

Lp1

High
Category
MCP Least Privilege
Confidence
98% confidence
Finding

This skill makes authenticated outbound network requests to Router9 and presigned storage URLs, yet the capability is reportedly undeclared. Hidden network access is especially sensitive because it can transfer user data and files off-host, making the mismatch a significant permission and transparency issue.

Content

No source excerpt is available for this finding.

Lp1

High
Category
MCP Least Privilege
Confidence
98% confidence
Finding

This skill makes authenticated outbound network requests to Router9 and presigned storage URLs, yet the capability is reportedly undeclared. Hidden network access is especially sensitive because it can transfer user data and files off-host, making the mismatch a significant permission and transparency issue.

Content

No source excerpt is available for this finding.

Lp1

High
Category
MCP Least Privilege
Confidence
98% confidence
Finding

This skill makes authenticated outbound network requests to Router9 and presigned storage URLs, yet the capability is reportedly undeclared. Hidden network access is especially sensitive because it can transfer user data and files off-host, making the mismatch a significant permission and transparency issue.

Content

No source excerpt is available for this finding.

Tainted flow: 'req' from pathlib.Path.read_bytes (line 160, file read) → urllib.request.urlopen (network output)

High
Category
Data Flow
Confidence
80% confidence
Finding

File contents flow to a network sink. This may indicate data exfiltration of sensitive files.

Content

Scanner excerpt · scripts/router9.py (reported line 47)May include surrounding context.

python
req = urllib.request.Request(
        f"{BASE_URL}{path}", data=data, headers=_headers("application/json")
    )
    with urllib.request.urlopen(req) as resp:
        return resp.read(), resp.headers.get("Content-Type", "")

Tainted flow: 'req' from pathlib.Path.read_bytes (line 160, file read) → urllib.request.urlopen (network output)

High
Category
Data Flow
Confidence
80% confidence
Finding

File contents flow to a network sink. This may indicate data exfiltration of sensitive files.

Content

Scanner excerpt · scripts/router9.py (reported line 56)May include surrounding context.

python
req = urllib.request.Request(
        f"{BASE_URL}{path}", data=data, headers=_headers("application/json")
    )
    with urllib.request.urlopen(req) as resp:
        return resp.read(), resp.headers.get("Content-Type", "")

Tainted flow: 'req' from pathlib.Path.read_bytes (line 160, file read) → urllib.request.urlopen (network output)

High
Category
Data Flow
Confidence
80% confidence
Finding

File contents flow to a network sink. This may indicate data exfiltration of sensitive files.

Content

Scanner excerpt · scripts/router9.py (reported line 62)May include surrounding context.

python
req = urllib.request.Request(
        f"{BASE_URL}{path}", data=data, headers=_headers("application/json")
    )
    with urllib.request.urlopen(req) as resp:
        return resp.read(), resp.headers.get("Content-Type", "")

Tainted flow: 'req' from pathlib.Path.read_bytes (line 160, file read) → urllib.request.urlopen (network output)

High
Category
Data Flow
Confidence
80% confidence
Finding

File contents flow to a network sink. This may indicate data exfiltration of sensitive files.

Content

Scanner excerpt · scripts/router9.py (reported line 68)May include surrounding context.

python
req = urllib.request.Request(
        f"{BASE_URL}{path}", data=data, headers=_headers("application/json")
    )
    with urllib.request.urlopen(req) as resp:
        return resp.read(), resp.headers.get("Content-Type", "")

Tainted flow: 'req' from pathlib.Path.read_bytes (line 160, file read) → urllib.request.urlopen (network output)

High
Category
Data Flow
Confidence
80% confidence
Finding

File contents flow to a network sink. This may indicate data exfiltration of sensitive files.

Content

Scanner excerpt · scripts/router9.py (reported line 165)May include surrounding context.

python
req = urllib.request.Request(
        f"{BASE_URL}{path}", data=data, headers=_headers("application/json")
    )
    with urllib.request.urlopen(req) as resp:
        return resp.read(), resp.headers.get("Content-Type", "")

Tainted flow: 'req' from pathlib.Path.read_bytes (line 160, file read) → urllib.request.urlopen (network output)

High
Category
Data Flow
Confidence
80% confidence
Finding

File contents flow to a network sink. This may indicate data exfiltration of sensitive files.

Content

Scanner excerpt · scripts/router9.py (reported line 205)May include surrounding context.

python
req = urllib.request.Request(
        f"{BASE_URL}{path}", data=data, headers=_headers("application/json")
    )
    with urllib.request.urlopen(req) as resp:
        return resp.read(), resp.headers.get("Content-Type", "")

Tainted flow: 'put_req' from pathlib.Path.read_bytes (line 173, file read) → urllib.request.urlopen (network output)

High
Category
Data Flow
Confidence
91% confidence
Finding

This code uploads arbitrary local file bytes to a presigned URL returned by the remote API without validating the destination host or scheme. If the upstream API were compromised or returned an unexpected URL, the tool could be turned into a file-exfiltration primitive to an attacker-controlled endpoint.

Content

Scanner excerpt · scripts/router9.py (reported line 176)May include surrounding context.

python
put_req = urllib.request.Request(upload_url, data=file_data, method="PUT")
    put_req.add_header("Content-Type", mime)
    try:
        with urllib.request.urlopen(put_req) as _resp:
            pass
    except urllib.error.HTTPError as e:
        # Clean up orphan DB record

Tainted flow: 'del_req' from pathlib.Path.read_bytes (line 181, file read) → urllib.request.urlopen (network output)

High
Category
Data Flow
Confidence
80% confidence
Finding

File contents flow to a network sink. This may indicate data exfiltration of sensitive files.

Content

Scanner excerpt · scripts/router9.py (reported line 185)May include surrounding context.

python
f"{BASE_URL}/v1/storage/files/{file_id}",
                method="DELETE", headers=_headers(),
            )
            urllib.request.urlopen(del_req)
        except Exception:
            pass
        print(json.dumps({"error": e.code, "message": "Upload to storage failed"}),

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The skill clearly sends user-provided text, audio, images, and files to Router9 cloud APIs for processing and storage, but the description does not warn about external transmission, retention, or privacy implications. This can cause users or higher-level agents to unknowingly upload sensitive content such as documents, photos, or recordings to a third-party service, creating confidentiality and compliance risks.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

Audio and image inputs are base64-encoded or uploaded and sent to a third-party API, but the code provides no explicit warning or confirmation about remote transmission. This creates a privacy and consent risk because users may treat the operations as local processing while sensitive media is actually sent off-device.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
98% confidence
Finding

The upload command can send any chosen local file to remote storage with no confirmation or warning that the file leaves the machine. In an agent context, this materially increases the danger because local secrets, documents, or credentials could be exfiltrated through a seemingly ordinary storage feature.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The delete command performs a destructive remote action immediately and without confirmation. In an agent setting, mistaken or manipulated invocation could permanently remove stored data the user did not intend to delete.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.