Back to skill

Security audit

openclaw-monitor

Security checks for vulnerabilities and agentic risk

Overview

The skill is mostly a coherent local monitoring tool, but it under-discloses sensitive telemetry exposure and relies on executable code outside the package boundary.

Review before installing. Treat generated interchange reports as potentially sensitive, especially ops/health.md, because failed task commands and errors may contain paths, identifiers, prompts, or secrets. Only restore from trusted backups, expect restore to overwrite the current monitoring database, and verify the missing external interchange dependency/package layout before running the CLI.

Vulnerability Patterns
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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
Findings (3)

T09 · Insecure Skill Coding Practices

Warning
Location
src/interchange.js:94
Finding
Raw Task Telemetry Is Published in a Shareable Operations Report<![CDATA[ ## Vulnerability Details **File Location**: `src/interchange.js:94-103` **Vulnerability Type**: Sensitive information exposure across a documented trust boundary **Risk Level**: Medium ### Vulnerable Code ```js const content = `# System Health ${overallIcon} **Overall:** ${taskHealthy && cronHealthy ? 'Healthy' : 'Degraded'} ## Subsystems - ${taskIcon} **Tasks:** ${taskTotal === 0 ? 'No activity today' : taskHealthy ? 'Healthy' : 'Failures detected'} - ${cronIcon} **Crons:** ${status.cron_ok + status.cron_fail === 0 ? 'No runs today' : cronHealthy ? 'All passing' : 'Failures detected'} - ${hasActivity ? '🟢' : '⚪'} **Token Collection:** ${hasActivity ? 'Active' : 'No data today'} ## Recent Errors ${status.recent_errors.length === 0 ? 'None' : status.recent_errors.map(e => `- ${e.command}: ${e.error || 'unknown error'}`).join('\n')} `; await writeMd(path.join(INTERCHANGE_DIR, 'ops', 'health.md'), meta, content); ``` The underlying values are selected without redaction in `src/reports.js:48-50`: ```js const recentErrors = db.prepare(` SELECT command, error, timestamp FROM task_events WHERE status != 'success' AND timestamp > datetime('now', '-7 days') ORDER BY timestamp DESC LIMIT 5 `).all(); ``` ### Technical Analysis The Skill documentation states that `interchange/monitoring/ops/health.md` is shareable and contains status indicators only, while detailed information is reserved for the private `state` layer. The implementation violates this boundary by publishing raw task command and error strings in the operations-layer report. Task commands and errors commonly contain file paths, user identifiers, request fragments, prompts, infrastructure names, exception details, or accidentally logged credentials. Neither the collector nor the report generator redacts these values before writing them to the shareable file. Although no network transmission is implemented, the README explicitly describes interchange reports as available for ...[truncated 1378 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove raw `command` and `error` values from `ops/health.md`; expose only aggregate indicators such as the number of recent failures. 2. Keep diagnostic details exclusively in the private `state` layer. 3. Apply centralized redaction for credentials, authorization headers, tokens, URLs containing secrets, email addresses, and sensitive paths. 4. Truncate diagnostic fields to a conservative maximum length. 5. Define and enforce an explicit schema for data permitted in each interchange layer. 6. Set restrictive filesystem permissions on private state reports and document the expected access-control model. 7. Add tests proving that distinctive command and error values never appear in operations-layer files. ]]>

T01 · Skill Instruction Hijacking

Error
Location
src/interchange.js:120
Finding
Unescaped Telemetry Enables Stored Markdown and Agent Instruction Injection<![CDATA[ ## Vulnerability Details **File Location**: `src/interchange.js:120-148` **Vulnerability Type**: Stored Markdown and downstream agent instruction injection **Risk Level**: High ### Vulnerable Code ```js const meta = { ...BASE_META, type: 'detail', layer: 'state', version: 1, tags: ['status', 'detail'], }; const cronTable = crons.length === 0 ? 'No cron data.' : ['| Job | Success | Failure | Last Run | Avg Duration |', '| --- | --- | --- | --- | --- |', ...crons.map(c => `| ${c.job_name} | ${c.success} | ${c.failure} | ${c.last_run} | ${c.avg_duration_ms}ms |`) ].join('\n'); const content = `# System Status (Detailed) ## Today's Token Spend - **Tokens In:** ${status.today_tokens_in} - **Tokens Out:** ${status.today_tokens_out} - **Cost:** $${status.today_cost.toFixed(4)} ## Task Success Rate - **Total:** ${taskTotal} - **Success:** ${status.task_success} | **Failure:** ${status.task_failure} | **Timeout:** ${status.task_timeout} - **Rate:** ${successRate}% ## Cron Health ${cronTable} ## Warnings ${status.recent_errors.length === 0 ? 'None' : status.recent_errors.map(e => `- ⚠️ ${e.command} failed: ${e.error || 'unknown'} (${e.timestamp})`).join('\n')} `; await writeMd(path.join(INTERCHANGE_DIR, 'state', 'status.md'), meta, content); ``` The same unsafe interpolation occurs in the shareable health report at `src/interchange.js:96`: ```js ${status.recent_errors.length === 0 ? 'None' : status.recent_errors.map(e => `- ${e.command}: ${e.error || 'unknown error'}`).join('\n')} ``` Attacker-controlled values enter storage through `src/collector.js:53-61` and `src/collector.js:68-78`: ```js export function addTaskEvent(db, event) { if (!VALID_TASK_STATUSES.has(event.status)) throw new Error(`Invalid task status: ${event.status}. Must be one of: ${[...VALID_TASK_STATUSES].join(', ')}`); if (event.duration_ms < 0) throw new Error('Duration must be non-negative'); const eventId = event.event_id || g ...[truncated 3814 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Treat every telemetry string as untrusted data, even when stored through prepared SQL statements. 2. Escape Markdown-special characters according to the output context: - Replace line breaks in table cells. - Escape pipe characters in tables. - Prevent values from introducing headings, lists, links, HTML, or fenced code boundaries. 3. Enforce strict maximum lengths and reject control characters for model names, skill names, task commands, errors, and cron names. 4. Represent untrusted diagnostics in explicitly delimited data blocks rather than mixing them into narrative Markdown. 5. Prefer schema-validated JSON for machine-to-machine interchange. If Markdown remains necessary, generate it only as a human-facing rendering of validated structured data. 6. Add metadata or consumer instructions stating that report payload fields are data and must never be followed as instructions. 7. Sanitize previously stored records before regenerating reports. 8. Add tests containing multiline values, pipes, headings, HTML, links, and instruction-like text to verify that none can escape the intended output structure. ]]>

T08 · Insecure Dependencies

Warning
Location
src/interchange.js:9
Finding
CLI Eagerly Loads an Undeclared Executable Dependency Outside the Package Boundary<![CDATA[ ## Vulnerability Details **File Location**: `src/interchange.js:9` **Vulnerability Type**: Unpinned external local-code dependency **Risk Level**: Medium ### Vulnerable Code ```js import { writeMd } from '../../interchange/src/index.js'; ``` The dependency is not declared in `package.json`, whose complete dependency list is: ```json "dependencies": { "better-sqlite3": "^11.0.0", "commander": "^12.0.0", "js-yaml": "^4.1.0" } ``` The CLI imports the interchange module eagerly at `src/cli.js:11`: ```js import { refreshInterchange } from './interchange.js'; ``` ### Technical Analysis From `src/interchange.js`, the path `../../interchange/src/index.js` resolves outside the audited project root. The referenced code is not included in the artifact and is neither declared nor pinned through the package manifest and lockfile. ES module imports execute top-level code during module initialization. Because `src/cli.js` imports `src/interchange.js` unconditionally, the external sibling module is loaded when any CLI command starts, not only when `monitor refresh` is invoked. This creates a local supply-chain boundary that package management does not protect. Installation behavior also depends on an undocumented sibling directory layout, reducing reproducibility and preventing the package lockfile from establishing the integrity of all executed code. ### Attack Path 1. An attacker gains the ability to create or modify the sibling path expected at `../../interchange/src/index.js`. This could occur through a compromised workspace component, unsafe extraction, shared writable directory, or another installer. 2. The attacker places top-level malicious JavaScript in that file while preserving a compatible `writeMd` export. 3. The user invokes any CLI command, such as: ```bash monitor status ``` 4. Node loads `src/cli.js`, which loads `src/interchange.js`. 5. Node evaluates the attacker-controlled sibling module before command dispatch. 6. Th ...[truncated 758 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Publish the interchange writer as a versioned package and declare it in `package.json`. 2. Pin the dependency through `package-lock.json` and verify package integrity during installation. 3. Alternatively, vendor the minimal required implementation inside this project so every executed source file remains within the audited package boundary. 4. If this is intentionally a workspace dependency, declare it explicitly using the package manager's workspace mechanism and enforce repository ownership and review controls. 5. Avoid relative imports that escape the package root. 6. Lazy-load refresh-only functionality after the `refresh` command is selected, reducing exposure of unrelated CLI commands. 7. Add a packaging test that installs the Skill into a clean directory and confirms that every runtime import resolves only to packaged or declared dependencies. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (14)

Known Vulnerable Dependency: js-yaml==4.1.1 — 4 advisory(ies): CVE-2026-84375 (js-yaml: maxTotalMergeKeys does not limit CPU use for empty merge sources); CVE-2026-59869 (js-yaml: YAML merge-key chains can force quadratic CPU consumption); GHSA-5p4m-2wfm-xmqj (JS-YAML: Quadratic CPU consumption in !!omap resolution (3.x and 4.x) — CVE-2026) +1 more

High
Category
Supply Chain
Confidence
96% confidence
Finding
The lockfile pins js-yaml to version 4.1.1, and the supplied advisories indicate known denial-of-service weaknesses involving excessive CPU consumption during YAML parsing. If this skill parses attacker-controlled or untrusted YAML input, an adversary could trigger resource exhaustion and cause the monitor/CLI process to hang or degrade service. The skill context increases concern because js-yaml is a declared direct dependency, suggesting YAML parsing is an intended feature rather than an unused transitive package.

Known Vulnerable Dependency: js-yaml==4.1.1 — 4 advisory(ies): CVE-2026-84375 (js-yaml: maxTotalMergeKeys does not limit CPU use for empty merge sources); CVE-2026-59869 (js-yaml: YAML merge-key chains can force quadratic CPU consumption); GHSA-5p4m-2wfm-xmqj (JS-YAML: Quadratic CPU consumption in !!omap resolution (3.x and 4.x) — CVE-2026) +1 more

High
Category
Supply Chain
Confidence
98% confidence
Finding
The manifest is reported as resolving to js-yaml 4.1.1, which has multiple advisories for CPU exhaustion via crafted YAML structures. In a monitoring/dashboard skill, YAML parsing may plausibly be used for configs or task data, so attacker-controlled or untrusted YAML could trigger denial of service and disrupt monitoring or automation workflows.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
This markdown file describes `monitor backup` and `monitor restore` operations, and restore in particular can affect existing monitoring data, but the README provides no user-facing warning about overwrite, recovery risk, or the need to verify the target data location. For markdown files, safety-relevant behaviors that could affect user data or system integrity should include a warning or cautionary note.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The skill advertises `monitor backup` and especially `monitor restore <file>` without any warning that these operations can overwrite or replace existing state. In an agent setting, users or downstream automation may invoke restore unsafely, leading to accidental data loss, rollback to stale state, or restoration from an untrusted file.

Missing User Warnings

Medium
Confidence
86% confidence
Finding
This markdown file lists `monitor backup` and `monitor restore` as available commands but provides no warning about the data integrity, overwrite, or recovery risks associated with restore operations. For markdown files, safety-relevant behaviors that can affect user data or system integrity should include a clear warning or caution.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The module docstring describes this file as the "Commander CLI entry point for openclaw-monitor," and the top-level command description says "OpenClaw monitoring dashboard," which implies a reporting/observability interface. However, the code also defines mutating commands that ingest events, compute aggregates, refresh interchange markdown files, and restore the database, so the documentation understates and mischaracterizes the command surface.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The restore command performs a destructive database restore immediately from a user-supplied file with no confirmation prompt, dry-run mode, or explicit force flag. In an operational environment, a mistyped command, wrong path, or automation error could overwrite valid monitoring data and cause irreversible data loss or rollback to attacker-controlled or stale state.

Intent-Code Divergence

Medium
Confidence
92% confidence
Finding
The file-level guarantee says ops files contain zero actual costs, token counts, or user data, but writeHealth() includes raw recent error strings and command names in ops/health.md. Error messages often contain sensitive operational details, arguments, paths, identifiers, or user-provided content, so this creates a confidentiality/integrity mismatch and may leak data into a supposedly sanitized interchange layer.

Missing User Warnings

Medium
Confidence
86% confidence
Finding
This code generates `state/status.md` with actual token counts and cost figures, which affects potentially sensitive usage/accounting data. While the top-level comment says ops files omit user data, there is no comparable warning, confirmation, or explicit disclosure here that state files will contain real usage and cost details.

Missing User Warnings

Medium
Confidence
83% confidence
Finding
The function writes a 7-day breakdown of token usage, costs, and top model information into `state/token-spend.md`. This is a file write involving potentially sensitive operational data, but the function lacks any warning comment, confirmation, or visible user disclosure about persisting those details.

Scope Creep

Low
Category
Excessive Agency
Content
### 3. [MEDIUM] Recent Errors in Status Not Time-Bounded
**File:** `src/reports.js` line 45 (getStatus)
**Problem:** `recent_errors` query fetches the last 5 failures all-time, not limited to recent periods (e.g., last 7 days). This could show outdated errors in daily status reports, reducing relevance.
**Fix:** Add a time filter to the query, e.g., WHERE timestamp > datetime('now', '-7 days') ORDER BY timestamp DESC LIMIT 5. Make the window configurable if needed.
Confidence
70% confidence
Finding
Skill's behavior or capabilities extend beyond its stated purpose. Scope creep allows an agent to perform actions unrelated to its documented functionality, increasing the attack surface.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"node": ">=18"
  },
  "dependencies": {
    "better-sqlite3": "^11.0.0",
    "commander": "^12.0.0",
    "js-yaml": "^4.1.0"
  }
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Unpinned Dependencies

Low
Category
Supply Chain
Content
},
  "dependencies": {
    "better-sqlite3": "^11.0.0",
    "commander": "^12.0.0",
    "js-yaml": "^4.1.0"
  }
}
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"dependencies": {
    "better-sqlite3": "^11.0.0",
    "commander": "^12.0.0",
    "js-yaml": "^4.1.0"
  }
}
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.