Back to skill

Security audit

Korea metropolitan bus alerts

Security checks for vulnerabilities and agentic risk

Overview

The skill has a coherent bus-alert purpose, but its persistent scheduled-agent prompts and DM-only delivery claims are not safely scoped or enforced.

Review this before installing if your Gateway agent has broad tools or if alerts could be registered from untrusted chat input. Use only trusted route, stop, and destination values, confirm cron job payloads carefully, and avoid running setup.py unless you are comfortable with it changing your user systemd Gateway override and restarting the service.

Vulnerability Patterns
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • 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 (3)

T01 · Skill Instruction Hijacking

Error
Location
scripts/cron_builder.py:69
Finding
Persistent Agent Prompt Injection Through Unvalidated Route Values<![CDATA[ ## Vulnerability Details **File Location**: `scripts/cron_builder.py:69-86`; data reaches persistent cron registration through `scripts/rule_wizard.py:151-225` **Vulnerability Type**: Persistent prompt injection and potential command injection **Risk Level**: High ### Vulnerable Code ```python def parse_routes(s: str) -> List[str]: parts = [p.strip() for p in (s or "").split(",") if p.strip()] if not parts: raise ValueError("routes required") return parts def build_prompt(city: str, node: str, routes: List[str]) -> str: routes_csv = ",".join(routes) # The agent will run the deterministic script and then output a clean summary. return ( "You are running a scheduled TAGO bus arrival alert.\n" "- Do not reveal any secrets.\n" "- Query TAGO arrivals using the helper script and format a short message.\n\n" "Steps:\n" f"1) Run: python3 korea-metropolitan-bus-alerts/scripts/tago_bus_alert.py arrivals --city {city} --node {node} --routes {routes_csv}\n" "2) Parse the JSON and produce a human-friendly summary (Korean).\n" "3) Output ONLY the final message text (no code blocks).\n\n" "Format example:\n" "[버스 알림]\n" "- 535: 3분(2정거장 전) / 다음 12분\n" "- 730: 5분 / 다음 18분\n" ) ``` The generated prompt is registered as a recurring Agent task: ```python routes = parse_routes(input("Routes (comma-separated, e.g. 535,730): ").strip()) ... job = { "name": name, "schedule": {"kind": "cron", "cron": spec.to_cron(), "tz": spec.tz}, "sessionTarget": "isolated", "payload": { "kind": "agentTurn", "message": build_prompt(city, node, routes), "deliver": True, "bestEffortDeliver": True, "channel": channel, "to": to, }, } ... cmd = [ exe, "cron", "add", "--name", name, "--cron", spec.to_cron(), "--tz", spec.tz, "--session", "isolated", "--mes ...[truncated 2480 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Strictly validate route identifiers before prompt construction. For example, allow only a bounded set of characters and lengths appropriate for Korean bus routes: ```python ROUTE_RE = re.compile(r"^[A-Za-z0-9가-힣-]{1,20}$") def parse_routes(value: str) -> List[str]: routes = [part.strip() for part in value.split(",") if part.strip()] if not routes: raise ValueError("routes required") if len(routes) > 20: raise ValueError("too many routes") for route in routes: if not ROUTE_RE.fullmatch(route): raise ValueError(f"invalid route identifier: {route!r}") return routes ``` 2. Apply similarly strict allowlists to `city` and `node`, including length limits and rejection of whitespace and control characters. 3. Reject newlines, carriage returns, null bytes, leading option markers, and shell metacharacters in all values embedded in Agent instructions. 4. Avoid constructing executable commands inside natural-language prompts. Store structured arguments and invoke the deterministic helper through a fixed non-shell tool interface. 5. If an Agent prompt must contain external values, serialize them as clearly delimited JSON data and explicitly state that the data must never be interpreted as instructions. 6. Add tests covering newline injection, command separators, leading `--` values, oversized input, and embedded instruction text. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/rule_wizard.py:165
Finding
DM-Only Delivery Policy Is Not Enforced<![CDATA[ ## Vulnerability Details **File Location**: `scripts/rule_wizard.py:165-186` **Vulnerability Type**: Insufficient destination validation and authorization **Risk Level**: Medium ### Vulnerable Code ```python # Delivery target channel = input("Deliver channel (e.g. telegram): ").strip() or "telegram" to = input("Deliver to (DM chat id / user id target): ").strip() if not to: raise RuntimeError("delivery target 'to' is required for DM-only") spec = ScheduleSpec(kind=schedule, hh=hh, mm=mm, tz=tz) job = { "name": name, "schedule": {"kind": "cron", "cron": spec.to_cron(), "tz": spec.tz}, "sessionTarget": "isolated", "payload": { "kind": "agentTurn", "message": build_prompt(city, node, routes), "deliver": True, "bestEffortDeliver": True, "channel": channel, "to": to, }, } ``` The file also claims: ```python # DM-only delivery is enforced by asking for (channel,to) explicitly. ``` ### Technical Analysis The implementation treats any non-empty destination as a valid direct-message target. Both `channel` and `to` are free-form values, and the code performs no verification that: - The destination represents a direct message rather than a group or public channel. - The destination belongs to the user registering the rule. - The selected messaging provider is supported. - The current operator is authorized to send recurring alerts to that destination. Prompting for a destination is not equivalent to enforcing a DM-only policy. The resulting unverified values are copied directly into the persistent cron payload and passed to `clawdbot cron add`. ### Attack Path 1. During rule registration, an operator enters a group, channel, or unrelated recipient identifier in the `to` field. 2. The application verifies only that the value is non-empty. 3. The untrusted destination is stored in the recurring cron job. 4. Each scheduled bus alert is delivered to the selected destination rather than ...[truncated 622 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Derive the registering user’s delivery identity from trusted Gateway session metadata instead of accepting an arbitrary destination. 2. Maintain an allowlist of supported messaging providers. 3. Use provider-specific APIs or Gateway metadata to verify that the destination is a private conversation. 4. Verify that the destination belongs to the user who initiated registration. 5. Reject group, broadcast-channel, and public-channel identifiers. 6. Display the resolved recipient identity before registration and require confirmation. 7. If non-DM delivery is later supported, make it a separate explicit mode with prominent privacy warnings and appropriate authorization checks. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/setup.py:107
Finding
Custom Secret File Path Is Not Honored by the Systemd Override<![CDATA[ ## Vulnerability Details **File Location**: `scripts/setup.py:107-134`; custom path accepted at `scripts/setup.py:160-170` **Vulnerability Type**: Incorrect secret-file configuration **Risk Level**: Low ### Vulnerable Code The setup interface accepts an arbitrary environment-file path: ```python ap = argparse.ArgumentParser() ap.add_argument("--env-file", default=str(DEFAULT_ENV_FILE), help="Where to store the env file") ap.add_argument("--unit", default=None, help="Override auto-detected gateway unit") ... args = ap.parse_args() env_file = Path(args.env_file).expanduser() ``` The key is written to that selected path, but the systemd override discards its directory: ```python def patch_override(unit: str, env_file: Path) -> None: override = systemd_override_path(unit) override.parent.mkdir(parents=True, exist_ok=True) content = "[Service]\n" + f"EnvironmentFile=%h/.clawdbot/secrets/{env_file.name}\n" # If override exists, keep it simple: replace any previous EnvironmentFile line for our tago file. if override.exists(): old = override.read_text(encoding="utf-8", errors="replace").splitlines(True) new_lines: List[str] = [] in_service = False env_written = False for ln in old: if ln.strip().startswith("["): in_service = ln.strip().lower() == "[service]" if in_service and ln.strip().startswith("EnvironmentFile=") and "tago.env" in ln: # drop old tago env reference continue new_lines.append(ln) # Ensure we have a [Service] block and our EnvironmentFile text = "".join(new_lines) if "[Service]" not in text and "[service]" not in text.lower(): text = text.rstrip() + "\n\n[Service]\n" if f"EnvironmentFile=%h/.clawdbot/secrets/{env_file.name}" not in text: text = text.rstrip() + f"\nEnvironmentFile=%h/.clawdbot/secrets/{env_file.name}\n" override.wri ...[truncated 1974 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Configure systemd with the exact path passed to `--env-file` rather than reconstructing it from the basename. 2. Resolve the path to an absolute path and escape it according to systemd unit-file syntax. 3. Alternatively, remove support for custom paths and require the documented fixed path under `~/.clawdbot/secrets/`. 4. Verify after writing the override that the `EnvironmentFile` directive resolves to the same file that received the new key. 5. Run `systemd-analyze --user verify` or an equivalent validation before restarting the Gateway. 6. Fail safely and avoid restarting the service if the selected file and configured file do not match. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (23)

Tainted flow: 'cmd' from input (line 205, user input) → subprocess.run (code execution)

Critical
Category
Data Flow
Content
def _run(cmd: List[str]) -> str:
    p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    if p.returncode != 0:
        raise RuntimeError(f"Command failed ({p.returncode}): {' '.join(cmd)}\n{p.stderr.strip()}")
    return p.stdout
Confidence
90% confidence
Finding
External input (network, user) flows to a code execution sink. This enables remote code execution or command injection.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The skill claims an end-user alerting purpose but documents actions that auto-detect gateway units, write systemd overrides, store secrets, and restart the Gateway service. In context, those are materially more sensitive administrative operations than bus alert management, and a description-behavior mismatch here could be used to socially engineer approval for impactful local changes.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The skill claims an end-user alerting purpose but documents actions that auto-detect gateway units, write systemd overrides, store secrets, and restart the Gateway service. In context, those are materially more sensitive administrative operations than bus alert management, and a description-behavior mismatch here could be used to socially engineer approval for impactful local changes.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The skill claims an end-user alerting purpose but documents actions that auto-detect gateway units, write systemd overrides, store secrets, and restart the Gateway service. In context, those are materially more sensitive administrative operations than bus alert management, and a description-behavior mismatch here could be used to socially engineer approval for impactful local changes.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The skill claims an end-user alerting purpose but documents actions that auto-detect gateway units, write systemd overrides, store secrets, and restart the Gateway service. In context, those are materially more sensitive administrative operations than bus alert management, and a description-behavior mismatch here could be used to socially engineer approval for impactful local changes.

Lp3

Medium
Category
MCP Least Privilege
Confidence
86% confidence
Finding
The skill advertises capabilities that include environment access, file write, network access, and shell execution, but it does not declare any explicit tool scope or allowed-tools boundary. In an agent ecosystem, missing least-privilege constraints increases the chance that the skill can invoke broader actions than users expect, especially because the document also instructs setup steps that modify local service configuration and restart services.

Session Persistence

Medium
Category
Rogue Agent
Content
---
name: korea-metropolitan-bus-alerts
description: Create and manage scheduled bus arrival alerts using Korea TAGO (국토교통부) OpenAPI and Clawdbot cron. Use when a user wants to register weekday/weekend schedules like "평일 오전 7시, <정류소명>, <노선들>" and receive automatic arrival summaries via their configured Gateway messaging (DM only).
metadata:
  {
    "openclaw": {
Confidence
72% confidence
Finding
The skill is explicitly designed to create persistent scheduled rules that continue running and delivering messages over time. Session persistence is expected for cron-based alerts, but it still introduces security and privacy risk because actions and messaging continue after the initiating interaction, and persistent jobs can be abused or forgotten if creation/deletion safeguards are weak.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The README tells users to place the TAGO service key directly in an export command and later source a file containing the secret, but it does not warn that inline shell commands may be recorded in shell history and that environment variables can be exposed to subprocesses, debug logs, or shared sessions. In this skill’s context, the key is an external API credential rather than a harmless value, so poor handling can lead to unauthorized API use, quota exhaustion, or key revocation.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
The generated prompt explicitly instructs the downstream agent to produce the summary in Korean. This is a natural-language locale policy constraint, and the file does not offer a language choice or explain that the skill is limited to Korean-only use.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def _run(cmd: List[str]) -> str:
    p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    if p.returncode != 0:
        raise RuntimeError(f"Command failed ({p.returncode}): {' '.join(cmd)}\n{p.stderr.strip()}")
    return p.stdout
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
Features:
- Auto-detect the Gateway systemd unit (supports custom names)
- Prompt for TAGO_SERVICE_KEY (hidden input)
- Save to an env file (default: ~/.clawdbot/secrets/tago.env, chmod 600)
- Write/patch a systemd override EnvironmentFile=...
- Restart the Gateway service
- Smoke-test TAGO API call
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
Features:
- Auto-detect the Gateway systemd unit (supports custom names)
- Prompt for TAGO_SERVICE_KEY (hidden input)
- Save to an env file (default: ~/.clawdbot/secrets/tago.env, chmod 600)
- Write/patch a systemd override EnvironmentFile=...
- Restart the Gateway service
- Smoke-test TAGO API call
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
Features:
- Auto-detect the Gateway systemd unit (supports custom names)
- Prompt for TAGO_SERVICE_KEY (hidden input)
- Save to an env file (default: ~/.clawdbot/secrets/tago.env, chmod 600)
- Write/patch a systemd override EnvironmentFile=...
- Restart the Gateway service
- Smoke-test TAGO API call
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
Features:
- Auto-detect the Gateway systemd unit (supports custom names)
- Prompt for TAGO_SERVICE_KEY (hidden input)
- Save to an env file (default: ~/.clawdbot/secrets/tago.env, chmod 600)
- Write/patch a systemd override EnvironmentFile=...
- Restart the Gateway service
- Smoke-test TAGO API call
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
Features:
- Auto-detect the Gateway systemd unit (supports custom names)
- Prompt for TAGO_SERVICE_KEY (hidden input)
- Save to an env file (default: ~/.clawdbot/secrets/tago.env, chmod 600)
- Write/patch a systemd override EnvironmentFile=...
- Restart the Gateway service
- Smoke-test TAGO API call
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
Features:
- Auto-detect the Gateway systemd unit (supports custom names)
- Prompt for TAGO_SERVICE_KEY (hidden input)
- Save to an env file (default: ~/.clawdbot/secrets/tago.env, chmod 600)
- Write/patch a systemd override EnvironmentFile=...
- Restart the Gateway service
- Smoke-test TAGO API call
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def run(cmd: List[str], *, check: bool = True) -> subprocess.CompletedProcess:
    return subprocess.run(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=check)


def list_gateway_units() -> List[str]:
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Description-Behavior Mismatch

Medium
Confidence
93% confidence
Finding
The setup script performs host-level discovery and modification of user systemd configuration, writes override files, and restarts a service. Even if intended for legitimate integration, this expands the skill from bus alerts into persistent local service reconfiguration, which can disrupt unrelated services or be abused if the chosen unit or env-file path is manipulated.

Context-Inappropriate Capability

Medium
Confidence
91% confidence
Finding
Restarting a systemd user service is a privileged operational action relative to the skill's stated purpose and can cause denial of service or unexpected behavior if the wrong unit is targeted. In a skill ecosystem, code that can reconfigure and restart host services increases trust requirements and blast radius beyond simple alert scheduling.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
if not script.exists():
        return

    p = subprocess.run(
        [sys.executable, str(script), "nearby-stops", "--lat", "37.5665", "--long", "126.9780"],
        text=True,
        stdout=subprocess.PIPE,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Natural-Language Policy Violations

Low
Confidence
94% confidence
Finding
The schedule mapping examples are presented only in Korean ("매일", "평일", "주말") even though the rest of the document is in English. This creates a language/locale constraint in the skill instructions without user opt-in or justification for why the skill is region- or language-specific.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
The script prints a Korean-only status header (`[버스 알림 테스트]`) in a general-purpose CLI flow. This imposes a specific language on users without any opt-in or documented locale constraint, which matches the language/locale policy concern.

Context-Inappropriate Capability

Low
Confidence
88% confidence
Finding
Enumerating local user services is not directly harmful, but it gathers host configuration details unrelated to bus alerts and is part of a broader capability to identify and modify services. In a semi-trusted plugin or skill context, unnecessary host introspection increases attack surface and can aid misuse.

Static analysis

No suspicious patterns detected.