Back to skill

Security audit

Ops Framework

Security checks for vulnerabilities and agentic risk

Overview

This skill is a disclosed operations monitor, but it can repeatedly run arbitrary configured local commands and send Telegram alerts without a strong technical boundary around its claimed read-only mode.

Review before installing. Use this only if you are comfortable treating ops-jobs.json and every referenced script as trusted code. Keep autoResume off unless needed, restrict file permissions on ~/.openclaw/net/config and scripts, run the monitor under a low-privilege account, avoid sending secrets in status output, and prefer explicit stop commands over PID-based stopping.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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
Findings (2)

T09 · Insecure Skill Coding Practices

Error
Location
ops-monitor.py:556
Finding
Self-Declared Risk Labels Do Not Enforce Read-Only Execution## Vulnerability Details **File Location**: `ops-monitor.py:304-330`, `ops-monitor.py:556-600` **Vulnerability Type**: Arbitrary command execution through unenforced trust labels **Risk Level**: High ### Vulnerable Code ```python risk = raw.get("risk") if not isinstance(risk, str) or risk not in ALLOWED_RISKS: raise ValueError(f"Invalid job {jid}: risk must be one of {sorted(ALLOWED_RISKS)}") cwd_raw = raw.get("cwd") if cwd_raw is None: cwd = OPENCLAW_HOME elif isinstance(cwd_raw, str) and cwd_raw.strip(): cwd = Path(cwd_raw).expanduser() else: raise ValueError(f"Invalid job {jid}: cwd must be a non-empty string path") commands_raw = raw.get("commands") if not isinstance(commands_raw, dict): raise ValueError(f"Invalid job {jid}: commands must be an object") commands: dict[str, list[str]] = {} for k, v in commands_raw.items(): if not isinstance(k, str): continue argv = _as_argv(v) if argv is None: raise ValueError(f"Invalid job {jid}: commands.{k} must be a non-empty argv list") commands[k] = argv ``` ```python def maybe_autorun_start( *, job: JobConfig, status: JobStatus, now: float, state_job: dict[str, Any], defaults: JobDefaults, dry_run: bool, ) -> str | None: if job.kind != "long_running_read": return None if not job.enabled: return None if job.risk != "read_only": return "AUTORUN: blocked (risk != read_only)" if status.running or status.completed: return None if not _policy_bool(job, defaults, "autoResume"): return None last = state_job.get("lastAutoResumeAt") try: last_ts = float(last) if isinstance(last, (int, float)) else 0.0 except Exception: last_ts = 0.0 backoff = float(_policy_int(job, defaults, "autoResumeBackoffSeconds") or 0) if backoff and now - last_ts < ba ...[truncated 3138 chars]
Remediation
## Remediation Suggestions 1. Explicitly document that the job configuration is executable trusted code, not a security policy boundary. 2. Restrict ownership and permissions on the configuration, state directory, and invoked scripts so only the dedicated monitor administrator can modify them. 3. Run the monitor under a dedicated, unprivileged operating-system account with access only to required paths. 4. Replace arbitrary command arrays for automatic execution with an allowlist of audited executable paths and fixed argument schemas. 5. Resolve executable paths to canonical absolute paths and reject writable, relative, unexpected, or symlink-substituted executables. 6. Store approved command manifests or hashes and verify their integrity before automatic execution. 7. Apply operating-system sandboxing to restrict filesystem writes, process access, and network destinations. 8. Disable networking for genuinely local read jobs unless a reviewed job explicitly requires it. 9. Keep `autoResume` disabled by default and require a separate, protected approval record rather than an approval field in the same mutable configuration. 10. Add negative tests proving that mislabeled write commands cannot modify files or access the network.

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
ops-monitor.py:1049
Finding
Unverified Status PID Can Terminate an Unrelated Process## Vulnerability Details **File Location**: `ops-monitor.py:416-444`, `ops-monitor.py:1049-1084` **Vulnerability Type**: Improper process authorization and PID validation **Risk Level**: Medium ### Vulnerable Code ```python def _status_from_json(obj: Any) -> JobStatus: if not isinstance(obj, dict): raise ValueError("status JSON must be an object") running = obj.get("running") completed = obj.get("completed") if not isinstance(running, bool) or not isinstance(completed, bool): raise ValueError("status JSON must include boolean fields: running, completed") pid = obj.get("pid") pid_i = int(pid) if isinstance(pid, int) and pid > 0 else None stop_reason = obj.get("stopReason") stop_reason = stop_reason if isinstance(stop_reason, str) and stop_reason.strip() else None level = obj.get("level") level = level if isinstance(level, str) and level.strip() else None message = obj.get("message") message = message if isinstance(message, str) and message.strip() else None progress = obj.get("progress") progress = progress if isinstance(progress, dict) else None progress_key = obj.get("progressKey") progress_key = progress_key if isinstance(progress_key, str) and progress_key.strip() else None if not progress_key and progress is not None: try: progress_key = json.dumps(progress, sort_keys=True, ensure_ascii=False) except Exception: progress_key = None return JobStatus( running=running, completed=completed, pid=pid_i, stop_reason=stop_reason, progress=progress, progress_key=progress_key, level=level, message=message, ) ``` ```python def cmd_stop(args: argparse.Namespace) -> int: now = _now_ts() defaults, jobs, state, state_jobs, queue, state_path = _load_for_cmd(args) _ = defaults ...[truncated 3403 chars]
Remediation
## Remediation Suggestions 1. Record the PID immediately when a job is started instead of trusting a later status response as the sole source of identity. 2. Record additional process identity attributes, including executable path, command line, owner, process start time, and a generated job-instance identifier. 3. Before signaling, verify that all recorded identity attributes still match the live process to prevent PID-reuse attacks. 4. Reject stop requests if process identity cannot be conclusively verified. 5. Prefer a dedicated, audited `commands.stop` implementation or an operating-system process supervisor that tracks process handles. 6. Run each job in a dedicated process group or service unit and stop that managed unit rather than an unverified numeric PID. 7. Run the monitor without elevated privileges and isolate unrelated jobs under separate operating-system accounts where practical. 8. Add tests covering malicious PIDs, stale PIDs, PID reuse, executable mismatches, and unrelated same-user processes.
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
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (20)

Credential Access

High
Category
Privilege Escalation
Content
## 提交前检查清单

- [ ] 不包含任何密钥(Telegram bot token、MCP access token、provider apiKey 等)
- [ ] 不包含任何个人路径(例如 `/Users/<name>/...`)与个人信息
- [ ] 示例配置使用占位路径(`/path/to/workdir`)与占位 chat id
- [ ] `python3 ops-monitor.py selftest` 通过
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Session Persistence

Medium
Category
Rogue Agent
Content
- “别动网盘里其他文件 / 只能读不能写” →
  - 只允许 read 类 MCP/命令
  - 路径范围严格限制(只触达 allowlist)
  - 任何 rename/move/delete/copy/overwrite 一律阻断并 `ACTION REQUIRED`
- “只整理这批 / 其他不动” →
  - 明确 allowlist;allowlist 外一律不触达
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Session Persistence

Medium
Category
Rogue Agent
Content
- “别动网盘里其他文件 / 只能读不能写” →
  - 只允许 read 类 MCP/命令
  - 路径范围严格限制(只触达 allowlist)
  - 任何 rename/move/delete/copy/overwrite 一律阻断并 `ACTION REQUIRED`
- “只整理这批 / 其他不动” →
  - 明确 allowlist;allowlist 外一律不触达
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Session Persistence

Medium
Category
Rogue Agent
Content
- Job Registry(配置):`~/.openclaw/net/config/ops-jobs.json`
- Runtime State(运行态):`~/.openclaw/net/state/ops-monitor.json`
- 工具脚本:`~/.openclaw/net/tools/ops-monitor.py`
- (可选)调度器(macOS launchd):`~/Library/LaunchAgents/ai.openclaw.ops-monitor.plist`

## Job Schema(v1)
Confidence
75% confidence
Finding
The framework explicitly defines persistent components: job registry files, runtime state, and an optional launchd agent for scheduled monitoring. This creates a host-level persistence mechanism that can survive across sessions and repeatedly execute jobs, which is dangerous if a malicious or misconfigured job is registered because it enables unattended recurring execution and ongoing monitoring.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill advertises operational capabilities involving local scripts, config/state files, shell commands, environment variables, and outbound Telegram messaging, but it declares no explicit tool scope or permissions boundary. That mismatch can cause an agent runtime or reviewer to underestimate the skill's effective access, increasing the risk of unintended file, shell, or network use.

Session Persistence

Medium
Category
Rogue Agent
Content
description: >-
  A 0-token jobs + monitoring framework for OpenClaw: run long-running read tasks
  via scripts, checkpoint/resume safely, and send periodic progress + immediate
  alerts to Telegram. Write jobs are blocked by default and must be explicitly
  approved and verified.
version: 0.1.0
author: Zjianru
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
- `long_running_read`
- `one_shot_read`
- `one_shot_write` (**never auto-executed by ops-monitor**)

`risk` is one of:
Confidence
85% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Description-Behavior Mismatch

Medium
Confidence
88% confidence
Finding
The manifest describes a framework centered on long-running read tasks where writes are blocked by default and require explicit approval and verification. However, the code formally includes `one_shot_write` and `write_local`/`write_external` as allowed job types/risks, validates write-job approval metadata, and exposes their approval status, showing that write operations are part of the supported framework semantics rather than being absent or universally blocked in code.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def send_telegram_via_openclaw(*, openclaw_bin: str, target: str, message: str) -> None:
    subprocess.run(
        [openclaw_bin, "message", "send", "--channel", "telegram", "--target", target, "--message", message],
        check=True,
        capture_output=True,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

External Transmission

Medium
Category
Data Exfiltration
Content
if not isinstance(token, str) or not token:
        raise RuntimeError("telegram.botToken missing; cannot send directly")

    url = f"https://api.telegram.org/bot{token}/sendMessage"
    payload = json.dumps({"chat_id": target, "text": message, "disable_web_page_preview": True}).encode("utf-8")
    req = Request(url, data=payload, headers={"Content-Type": "application/json"})
    with urlopen(req, timeout=20) as r:  # noqa: S310
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def run_cmd(argv: list[str], *, cwd: Path, timeout_seconds: int) -> CmdResult:
    t0 = time.time()
    proc = subprocess.run(  # noqa: S603
        argv,
        cwd=str(cwd),
        capture_output=True,
Confidence
92% confidence
Finding
This helper executes arbitrary argv supplied by job configuration, making the framework a general command runner for configured tasks. Because config and working directory are externally controlled inputs, anyone who can modify the ops-jobs file or referenced scripts can achieve code execution under the monitor's privileges.

Tainted flow: 'argv' from os.environ.get (line 1028, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Content
def run_cmd(argv: list[str], *, cwd: Path, timeout_seconds: int) -> CmdResult:
    t0 = time.time()
    proc = subprocess.run(  # noqa: S603
        argv,
        cwd=str(cwd),
        capture_output=True,
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.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
if not argv:
        # Non-long-running jobs may not define status. Treat as not running.
        return JobStatus(running=False, completed=False, message="missing status command")
    proc = subprocess.run(  # noqa: S603
        argv,
        cwd=str(job.cwd),
        capture_output=True,
Confidence
90% confidence
Finding
The status command is executed directly from job configuration, so a supposedly harmless monitoring action can run arbitrary local programs every polling cycle. In this framework context, that meaningfully expands risk because periodic monitoring may repeatedly execute attacker-controlled code if the config is tampered with.

Tainted flow: 'argv' from os.environ.get (line 1028, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Content
if not argv:
        # Non-long-running jobs may not define status. Treat as not running.
        return JobStatus(running=False, completed=False, message="missing status command")
    proc = subprocess.run(  # noqa: S603
        argv,
        cwd=str(job.cwd),
        capture_output=True,
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.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
return "AUTORUN: would start (dry-run)"

    try:
        proc = subprocess.run(  # noqa: S603
            argv,
            cwd=str(job.cwd),
            capture_output=True,
Confidence
94% confidence
Finding
Auto-resume can launch configured start commands automatically without an interactive checkpoint, which turns a monitoring tick into autonomous code execution. If an attacker can alter job definitions or referenced scripts, they can get repeated execution with minimal operator involvement.

Tainted flow: 'argv' from os.environ.get (line 1028, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Content
return "AUTORUN: would start (dry-run)"

    try:
        proc = subprocess.run(  # noqa: S603
            argv,
            cwd=str(job.cwd),
            capture_output=True,
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.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The report-building logic emits fixed Chinese strings such as "状态", "信息", and "需要你确认下一步", and later the tool also sends Chinese alert headers. This imposes a specific language on all users without opt-in or any documented region-specific justification, which violates the natural-language locale policy.

Intent-Code Divergence

Medium
Confidence
98% confidence
Finding
The inline comment says one-shot jobs are executed explicitly or via a future queue/scheduler and are not polled here. In reality, earlier in the same `tick` function, the code processes queued one-shot read jobs immediately, updates queue state, executes commands, and reports results, so the comment actively misstates current behavior.

Natural-Language Policy Violations

Low
Confidence
94% confidence
Finding
The README's user-facing instructions and descriptions are written in Chinese, and there is no indication that alternative languages are available or that the locale restriction is intentional. Under the policy rule, forcing a specific language without user opt-in is a natural-language policy concern.

Natural-Language Policy Violations

Low
Confidence
96% confidence
Finding
Line L03 presents the core descriptive content in Chinese with no indication that alternate languages are supported or that the locale is intentionally restricted. This is a natural-language policy concern because it imposes a specific language on readers without opt-in or a documented regional justification.

Static analysis

No suspicious patterns detected.