Back to skill

Security audit

Mode Switch Kit

Security checks for vulnerabilities and agentic risk

Overview

The skill is coherent and mostly disclosed, but it can change VPN state and stop/start local processes with weak safeguards that could affect unrelated processes or connectivity.

Review the YAML carefully before use, run only selftests and dry runs first, and avoid running the switcher with administrator privileges. Do not schedule the watchdog or use real VPN/service commands until process ownership checks and VPN unknown-state handling are tightened, because a mode switch can disrupt connectivity or terminate unrelated local processes.

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 (3)

T09 · Insecure Skill Coding Practices

Error
Location
templates/modes_switch.py:387
Finding
Port-Only Service Identification Can Terminate Unrelated Processes<![CDATA[ ## Vulnerability Details **File Location**: `templates/modes_switch.py:387-416` **Vulnerability Type**: Unverified process ownership before termination **Risk Level**: High ### Vulnerable Code ```python def ensure_service(cfg, base, svc, want, dry=False, log=print): name, port = svc.get("name"), int(svc.get("port")) if want: if port_alive(port): log(f"service {name}:{port} running") return start_argv = svc.get("start_argv") if not start_argv: log(f"service {name}:{port} down, no start_argv (monitor only)") return if dry: log(f"service {name}:{port} WOULD start {start_argv}") return _spawn(start_argv, log=log, what=f"service {name}") deadline = time.time() + SERVICE_WAIT_S while time.time() < deadline and not port_alive(port): time.sleep(0.5) log(f"service {name}:{port} {'up' if port_alive(port) else 'did NOT come up'}") else: if not port_alive(port): log(f"service {name}:{port} already offline") return pids = _listen_pids(port) if dry: log(f"service {name}:{port} WOULD stop pid(s) {pids}") return if not pids: log(f"service {name}:{port} up but no owner pid found - left alone") return ok = all(stop_pid(p) for p in pids) log(f"service {name}:{port} {'stopped' if ok else 'stop FAILED'}") ``` ### Technical Analysis The switcher assumes that every process listening on a configured TCP port is the configured service. Although `_listen_pids()` performs an exact port comparison and therefore avoids substring errors such as confusing port `3001` with `30010`, the port number does not prove process ownership. The code does not verify any of the following before calling `stop_pid()`: - Whether the process was launched by this Skill. - Whether its executable path matches the configur ...[truncated 1718 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Record a service ownership file when `_spawn()` starts a service. It should contain the PID, process creation time, normalized executable path, expected command line, and a cryptographically random instance token. - Before termination, require the live process to match the recorded PID and creation time. Where supported, also compare executable path and command line. - Treat a port as a health signal only, not as authorization to terminate its owner. - If no trusted ownership record exists, leave the listener running and report that ownership could not be established. - Store ownership records in a directory writable only by the account running the switcher. - Avoid running the switcher as an administrator unless management of privileged services is explicitly required. - Add a regression test in which an unrelated process occupies a configured port and verify that mode switching refuses to terminate it. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
templates/modes_switch.py:274
Finding
Weak PID-File Validation Can Target Reused or Attacker-Selected Processes<![CDATA[ ## Vulnerability Details **File Location**: `templates/modes_switch.py:274-306, 370-384` **Vulnerability Type**: Unsafe trust in stale or attacker-controlled process identifiers **Risk Level**: High ### Vulnerable Code ```python def read_pidfile(cfg, base, profile): """Returns (pid, start_time). A bare integer pidfile is accepted.""" try: with open(pidfile_path(cfg, base, profile), encoding="utf-8") as f: data = json.load(f) except Exception: return None, None if isinstance(data, dict): return data.get("pid"), data.get("start_time") if isinstance(data, int): return data, None return None, None def gateway_alive(cfg, base, profile): """pidfile pid must exist AND (when a start_time is recorded) match the live process — a reused PID otherwise looks exactly like a healthy agent.""" pid, start_time = read_pidfile(cfg, base, profile) if not pid or not pid_alive(pid): return False if not start_time: return True live = _process_start_ms(pid) if live is None: return True tol = PID_FRESH_S * 1000 return any(abs(live - int(start_time) * scale) <= tol for scale in (1, 10, 1000)) ``` The resulting PID is later used for termination: ```python else: if not alive: log(f"agent {profile}: already offline") return pid, _ = read_pidfile(cfg, base, profile) if dry: log(f"agent {profile}: WOULD stop pid {pid}") return ok = stop_pid(pid) log(f"agent {profile}: {'stopped' if ok else 'stop FAILED (pid ' + str(pid) + ')'}") ``` ### Technical Analysis A PID is a temporary system identifier and does not establish process identity. Operating systems reuse PIDs, so a stale PID file may eventually refer to an unrelated process. The implementation accepts a bare integer PID file. When no creation timestamp is present, `gateway_alive()` considers any live proc ...[truncated 1879 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Do not authorize destructive actions using a bare PID file. - Require structured PID records containing at least the PID and process creation time. - Add platform-specific creation-time verification for every supported operating system. - If creation-time or identity lookup fails, refuse to terminate the process rather than treating it as valid. - Verify additional stable properties where available, including executable path, expected command line, user identity, and a random instance token. - Read and validate the PID record once immediately before termination to reduce time-of-check/time-of-use inconsistencies. - Reject invalid PID ranges and explicitly prevent targeting PID 0, PID 1, the switcher's own PID, and protected system processes. - Ensure profile and PID-file directories are writable only by their intended owner. - Clearly mark non-Windows process termination as unsupported until equivalent identity verification is implemented. - Add tests for stale PID reuse, tampered PID files, failed process-time lookup, and PID-file replacement between validation and termination. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
templates/modes_switch.py:326
Finding
VPN Status Failures Are Incorrectly Treated as a Disconnected State<![CDATA[ ## Vulnerability Details **File Location**: `templates/modes_switch.py:326-359` **Vulnerability Type**: Fail-open handling of an unknown security-sensitive network state **Risk Level**: Medium ### Vulnerable Code ```python def set_vpn(cfg, base, desired, dry=False, log=print): vpn = cfg.get("vpn") or {} cmd = _argv(vpn.get("cmd")) if desired is True: desired = "on" elif desired is False: desired = "off" desired = desired or "keep" if not cmd: log("vpn: not managed by this config (cmd is empty)" if desired != "keep" else "vpn: keep") return def current(): try: out = _run(cmd + _argv(vpn.get("status_args") or ["status"]), timeout=8) return vpn.get("connected_marker", "Connected") in (out.stdout or "") except Exception: return False up = current() args = None if desired == "on" and not up: args = _argv(vpn.get("on_args") or ["connect"]) elif desired == "off" and up: args = _argv(vpn.get("off_args") or ["disconnect"]) if args is None: log(f"vpn: already {desired} ({'up' if up else 'down'})") return if dry: log(f"vpn: WOULD run {' '.join(cmd + args)}") return _run(cmd + args, timeout=10) log(f"vpn: {' '.join(args)} -> requested") if desired == "on": time.sleep(int(vpn.get("settle_s") or 0)) ``` ### Technical Analysis VPN state has at least three meaningful values: connected, disconnected, and unknown. The implementation collapses unknown into disconnected by returning `False` whenever the status command raises an exception. It also does not inspect `out.returncode`. A VPN status command that exits unsuccessfully but produces no connected marker is likewise interpreted as disconnected. This contradicts the project's own documented safety requirement that a failed status query must not be treated as a disconnected state. VPN mutation is security-sensiti ...[truncated 1477 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Represent VPN state explicitly as `UP`, `DOWN`, or `UNKNOWN`. - Treat exceptions, timeouts, nonzero exit codes, malformed output, and ambiguous output as `UNKNOWN`. - When state is `UNKNOWN`, perform no connect or disconnect action by default. - Log the status failure clearly and return a nonzero exit code from the mode switch. - Optionally invoke the configured alert channel for unknown VPN state. - Add an explicit, opt-in override if users need mutation during unknown state; do not make it the default. - After a connect or disconnect request, poll status until the requested state is confirmed or a bounded timeout expires. - Add tests for missing executables, timeouts, nonzero status exits, ambiguous output, and failed post-transition verification. ]]>
Vulnerability Patterns
  • Memory PoisoningPersistent Context Injection, Context Window Stuffing, Memory Manipulation
  • 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
Findings (7)

Ae1

High
Category
analysis-evasion
Content
| `SKILL.md` | this file |
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Memory Manipulation

High
Category
Memory Poisoning
Content
assert load_wd_state(base_cfg, tmp) == {}, "missing state file -> fail open"
    with open(wd_state_path(base_cfg, tmp), "w", encoding="utf-8") as f:
        f.write("{not json")
    assert load_wd_state(base_cfg, tmp) == {}, "corrupt state file -> fail open"
    save_wd_state(base_cfg, tmp, {"repair_ts": [1.0]})
    assert load_wd_state(base_cfg, tmp) == {"repair_ts": [1.0]}, "round trip"
    import shutil
Confidence
90% confidence
Finding
Skill manipulates agent memory, state, or stored context. Memory corruption can alter personality, override safety rules, or cause unpredictable behavior.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
This markdown file states that one command can flip the whole machine stack, including VPN, background agents, local services, and model tier, but it does not include any warning about service disruption, connectivity changes, or other impacts to the user's system. For markdown files, descriptions that imply changes affecting system integrity or availability should disclose those risks.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
This markdown file includes a command to 'flip the real machine' and elsewhere explains that modes control VPN state, background agents, and local services. Although functionality is described, there is no direct user warning near the real-machine command that running it will actively stop or start components and may disrupt connectivity or running workflows.

Lp3

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding
The skill describes and instructs use of local scripts that read and write files and execute shell commands, but it does not declare any explicit tool scope such as permissions or allowed-tools. That creates a mismatch between documented behavior and enforcement, increasing the risk that an agent runtime grants broader-than-expected filesystem and command execution access when the skill is invoked.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def _run(argv, timeout=15, cwd=None):
    return subprocess.run(argv, capture_output=True, text=True, timeout=timeout,
                          cwd=cwd, encoding="utf-8", errors="replace",
                          creationflags=_NO_WINDOW)
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
kwargs["creationflags"] = _DETACH
    else:
        kwargs[_ON_WINDOWS_FLAG] = True
    subprocess.Popen([str(a) for a in argv], **kwargs)
    log(f"started {what or argv[0]}")
Confidence
85% confidence
Finding
This code launches detached background processes from configuration-controlled argv and cwd values without any trust boundary enforcement, signature check, or executable allowlist. In the context of a skill that explicitly starts and stops VPNs, agents, and services, a malicious or tampered modes.yaml can cause arbitrary code execution whenever a mode switch occurs, and the detached/no-console design makes such execution less visible to the user.

Static analysis

No suspicious patterns detected.