Back to skill

Security audit

Live Sessions Dashboard

Security checks for vulnerabilities and agentic risk

Overview

This dashboard appears intended for OpenClaw session monitoring, but it reads sensitive log/session data by default and can save or share raw session identifiers in HTML.

Install only if the operator is comfortable giving the skill access to OpenClaw session metadata. Prefer running the CLI monitor with --no-subscribe, do not rely on AGENT_MONITOR_NO_SUBSCRIBE unless the code is fixed, and avoid remote hosting of generated HTML unless identifiers are removed and access is protected. Treat generated dashboard files as sensitive operational data and clear or redact them before sharing.

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

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/agents_canvas_snapshot.py:112
Finding
Sensitive session metadata is persisted in a shareable HTML artifact<![CDATA[ ## Vulnerability Details **File Location**: `scripts/agents_canvas_snapshot.py:112-155, 264-277`; generated data visible in `assets/agents_canvas.html:63-84` **Vulnerability Type**: Sensitive operational metadata exposure **Risk Level**: Medium ### Vulnerable Code ```python for session in sessions: key = html.escape(session.get("key", session.get("sessionId", "unknown"))) age_ms = session.get("ageMs") status = session.get("status") or classify_status(age_ms) status_counts.setdefault(status, 0) status_counts[status] += 1 updated_ms = session.get("updatedAt") lag_seconds = None last_update_local = "-" last_update_ago = "-" if updated_ms: lag_seconds = max(0.0, now_epoch - (updated_ms / 1000.0)) last_update_local = datetime.fromtimestamp(updated_ms / 1000.0).strftime("%Y-%m-%d %H:%M:%S") last_update_ago = f"{human_duration(lag_seconds)} ago" lag_total += lag_seconds lag_count += 1 created_ms = session.get("createdAt") runtime_display = "-" if created_ms: runtime_seconds = max(0.0, now_epoch - (created_ms / 1000.0)) runtime_display = human_duration(runtime_seconds) longest_runtime = max(longest_runtime, runtime_seconds) tokens = session.get("totalTokens") tokens_total += tokens or 0 tokens_display = format_tokens(tokens) cost_display = format_cost(tokens, args.cost_per_1k) model = html.escape(session.get("model", "-")) kind = html.escape(session.get("kind", "-")) status_badge = f"<span class='status {STATUS_CLASSES.get(status, 'status-run')}'>{status}</span>" cost_cell = f"<td>{html.escape(cost_display)}</td>" if args.cost_per_1k > 0 else "" rows_html.append( "<tr>" f"<td>{key}</td>" f"<td>{status_badge}</td>" f"<td>{runtime_display}</td>" f"<td>{last_update_ago}</td>" f"<td>{last_update_local}</td>" f"<td>{tokens_display}</td>" f"{cost_ ...[truncated 3263 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Make the default dashboard aggregate-only and require an explicit flag such as `--include-identifiers` before including raw session keys. 2. Replace session and channel identifiers with truncated, salted hashes suitable only for distinguishing rows within the dashboard. 3. Write generated snapshots to a user-selected data directory outside the Skill source tree instead of `assets/` by default. 4. Replace the populated bundled HTML file with an empty template or synthetic sample data. 5. Add an explicit warning before generating remotely shareable output and document that static hosting must use authentication and transport encryption. 6. Provide field-level controls for timestamps, models, token usage, costs, and channel identifiers. 7. Set restrictive file permissions where supported and document snapshot retention and secure deletion. 8. Correct the documentation so that it distinguishes in-memory CLI monitoring from persistent HTML generation. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/agents_cli_monitor.py:357
Finding
Full sensitive log access is enabled by default despite poll-only functionality<![CDATA[ ## Vulnerability Details **File Location**: `scripts/agents_cli_monitor.py:145-147, 160-190, 357-360` **Vulnerability Type**: Excessive privilege and unnecessary sensitive-data access **Risk Level**: Medium ### Vulnerable Code ```python parser.add_argument("--once", action="store_true", help="Render a single snapshot and exit") parser.add_argument("--no-subscribe", action="store_true", help="Disable log subscription (poll-only mode)") return parser.parse_args() ``` ```python def tail_logs(monitor: SessionMonitor, stop_event: threading.Event) -> None: cmd = ["openclaw", "logs", "--json", "--follow", "--plain", "--interval", "1000"] proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, bufsize=1) def _cleanup() -> None: if proc.poll() is None: proc.terminate() try: proc.wait(timeout=1) except subprocess.TimeoutExpired: proc.kill() atexit.register(_cleanup) try: while not stop_event.is_set(): line = proc.stdout.readline() if not line: if proc.poll() is not None: break continue try: payload = json.loads(line) except json.JSONDecodeError: continue session_id = extract_session_id(payload) if session_id: monitor.touch_session(session_id) finally: _cleanup() ``` ```python if not args.no_subscribe: log_thread = threading.Thread(target=tail_logs, args=(monitor, stop_event), daemon=True) log_thread.start() ``` ### Technical Analysis The monitor automatically executes `openclaw logs --json --follow` unless the user supplies `--no-subscribe`. The Skill documentation acknowledges that these logs may contain prompts, tool arguments, secrets, and personally identifiable information. Neverthele ...[truncated 2249 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Make polling the default and require an explicit `--subscribe` option to enable log consumption. 2. Implement `AGENT_MONITOR_NO_SUBSCRIBE` as documented, while preserving poll-only behavior when the variable is set. 3. Prefer a session-state or session-event API that returns only session identifiers and activity timestamps. 4. If log subscription is enabled, clearly display a startup warning describing the sensitive data scope. 5. Request gateway permissions limited to session metadata rather than general log access where OpenClaw supports scoped authorization. 6. Avoid retaining full parsed payloads, and ensure exception reporting, tracing, and crash diagnostics cannot serialize log records. 7. Add automated tests confirming that the default invocation and environment-variable opt-out do not launch `openclaw logs`. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/agents_cli_monitor.py:339
Finding
Untrusted session metadata can inject terminal control sequences<![CDATA[ ## Vulnerability Details **File Location**: `scripts/agents_cli_monitor.py:256-278, 339-355` **Vulnerability Type**: Terminal escape-sequence injection **Risk Level**: Low ### Vulnerable Code ```python rows.append({ "key": data.get("key", key), "status": status, "runtime": runtime, "runtime_seconds": runtime_seconds, "start": start_time, "updated": updated_display, "lag_seconds": lag_seconds or 0.0, "tokens": format_tokens(total_tokens), "raw_tokens": raw_tokens, "cost": format_cost(total_tokens, args.cost_per_1k), "model": data.get("model", "-"), "kind": data.get("kind", "-"), }) ``` ```python for row in rows: status_field = f"{row['status']:<{col_widths['status']}}" parts = [ f"{ellipsize(row['key'], col_widths['key']):<{col_widths['key']}}", colorize(status_field, COLORS.get(row['status'], '')), f"{row['runtime']:<{col_widths['runtime']}}", f"{row['start']:<{col_widths['start']}}", f"{row['updated']:<{col_widths['updated']}}", f"{row['tokens']:<{col_widths['tokens']}}", ] if args.cost_per_1k > 0: parts.append(f"{row['cost']:<{col_widths['cost']}}") parts.extend([ f"{ellipsize(row['model'], col_widths['model']):<{col_widths['model']}}", f"{ellipsize(row['kind'], 10):<10}", ]) print(" ".join(parts)) ``` ### Technical Analysis The `key`, `model`, and `kind` values originate from OpenClaw session metadata and are sent to the terminal without removing ANSI escape sequences or C0/C1 control characters. `ellipsize()` limits Python character count but does not neutralize terminal commands. A malicious or compromised metadata source could include carriage returns, line erasure, cursor movement, OSC sequences, or other terminal controls. The terminal interprets those bytes as commands rather than visible text. This is distinct from the monitor's own intentional color sequences because externally sourced fie ...[truncated 1266 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Sanitize every externally sourced string before calculating its width or sending it to the terminal. 2. Remove ANSI CSI/OSC sequences and all C0/C1 controls except deliberately permitted whitespace. 3. Replace carriage return, newline, tab, and other formatting characters with visible escaped representations. 4. Apply a conservative printable-character allowlist where practical. 5. Keep application-generated color codes separate from sanitized data so only known constant sequences can reach the terminal. 6. Add tests containing escape, cursor-control, OSC, carriage-return, and newline payloads in each displayed metadata field. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/agents_canvas_snapshot.py:116
Finding
Gateway-provided status is embedded into generated HTML without escaping<![CDATA[ ## Vulnerability Details **File Location**: `scripts/agents_canvas_snapshot.py:116-120, 144-145` **Vulnerability Type**: Persistent HTML injection **Risk Level**: Low ### Vulnerable Code ```python for session in sessions: key = html.escape(session.get("key", session.get("sessionId", "unknown"))) age_ms = session.get("ageMs") status = session.get("status") or classify_status(age_ms) status_counts.setdefault(status, 0) status_counts[status] += 1 ``` ```python status_badge = f"<span class='status {STATUS_CLASSES.get(status, 'status-run')}'>{status}</span>" cost_cell = f"<td>{html.escape(cost_display)}</td>" if args.cost_per_1k > 0 else "" ``` ### Technical Analysis The session `status` value is accepted directly from the gateway whenever present. It is inserted into the HTML element body without `html.escape()`. Other textual fields such as the key, model, kind, and cost display are escaped, but status is not. The CSS class is selected through a fixed mapping and is not directly injectable, but the visible content between the `<span>` tags remains unsafe. A status such as `</span><script>...</script><span>` would become executable markup in the generated snapshot. Because the payload is written to disk and remains present until the next successful regeneration, this is a persistent HTML injection within the dashboard artifact. Exploitation requires control over, or compromise of, the gateway response or another upstream source of session status values. ### Attack Path 1. An attacker or compromised OpenClaw gateway supplies a session object with a malicious `status` string. 2. `fetch_sessions()` accepts the JSON response without schema or enum validation. 3. `render_html()` uses the supplied value rather than locally classifying the status. 4. The raw value is inserted between the status `<span>` tags. 5. The generated HTML file is opened in a browser or presented through Canvas. 6. The browser parses and executes the injected ...[truncated 637 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Enforce a strict status allowlist such as `RUN`, `IDLE`, `STALE`, and `EXITED`. 2. Map unknown values to a fixed safe label such as `UNKNOWN` rather than rendering the original input. 3. Apply `html.escape(str(status), quote=True)` before inserting any status text into HTML. 4. Validate the complete sessions payload against an expected schema, including field types and maximum lengths. 5. Add a restrictive Content Security Policy to generated pages, for example disallowing scripts entirely where browser presentation permits it. 6. Add regression tests using HTML tags, event handlers, entity payloads, and closing-tag injection in every gateway-provided string field. ]]>
Vulnerability Patterns
  • 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
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (4)

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill instructs users to execute local shell commands and describes scripts that write HTML files and may use environment variables, but it does not declare any explicit tool scope such as permissions or allowed-tools. That mismatch weakens security review and consent boundaries because an agent may be induced to use shell, read environment-derived configuration, and write files without those capabilities being clearly surfaced in metadata.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
cmd += ["--agent", args.agent]
    if args.all_agents:
        cmd.append("--all-agents")
    out = subprocess.check_output(cmd, text=True)
    payload = json.loads(out)
    return payload.get("sessions", [])
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
cmd += ["--agent", args.agent]
    if args.all_agents:
        cmd.append("--all-agents")
    out = subprocess.check_output(cmd, text=True)
    payload = json.loads(out)
    return payload.get("sessions", [])
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
def tail_logs(monitor: SessionMonitor, stop_event: threading.Event) -> None:
    cmd = ["openclaw", "logs", "--json", "--follow", "--plain", "--interval", "1000"]
    proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, bufsize=1)

    def _cleanup() -> None:
        if proc.poll() is None:
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.