Back to skill

Security audit

Lemnos Cost Guard

Security checks for vulnerabilities and agentic risk

Overview

The skill is a cost-tracking utility, but it overstates automatic budget enforcement and includes scripts with broad session-log access plus an unsafe snapshot deletion path.

Review before installing. This skill is not clearly malicious and I found no network exfiltration or remote-code loading, but it needs sensitive access to OpenClaw session logs and has an unsafe local file deletion bug. Treat its budget alerts as advisory, not enforced limits, and avoid putting secrets or customer details in task descriptions until reporting output is redacted and snapshot IDs are validated.

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
scripts/task_logger.py:113
Finding
Path Traversal Enables JSON File Access and Deletion Outside the Snapshot Directory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/task_logger.py`, lines 113–122 and 211–224 **Vulnerability Type**: Path traversal and unsafe file deletion **Risk Level**: High ### Vulnerable Code ```python def load_snapshot(snap_id: str) -> dict: snap_path = os.path.join(SNAPSHOT_DIR, f"{snap_id}.json") with open(snap_path) as f: return json.load(f) def delete_snapshot(snap_id: str): snap_path = os.path.join(SNAPSHOT_DIR, f"{snap_id}.json") if os.path.exists(snap_path): os.remove(snap_path) ``` The vulnerable functions are reached directly through the `end` command: ```python def cli_end(snap_id: str, status: str = "ok"): snap = load_snapshot(snap_id) after = read_session_totals() entry = log_task( task_type=snap["task_type"], description=snap["description"], before=snap["totals_before"], after=after, timestamp_start=snap["timestamp_start"], timestamp_end=datetime.now(timezone.utc).isoformat(), status=status ) delete_snapshot(snap_id) print(f"[task_logger] LOGGED: {snap['task_type']} | ${entry['cost_usd']:.4f} | " f"{entry['calls']} calls | {entry['output_tokens']:,} out tokens") ``` ```python elif cmd == "end": snap_id = sys.argv[2] status = sys.argv[3] if len(sys.argv) > 3 else "ok" cli_end(snap_id, status) ``` ### Technical Analysis The `snap_id` command-line argument is incorporated into a filesystem path without validation or canonicalization. The code assumes that the value is an eight-character UUID fragment, but this constraint is only applied when the program creates a snapshot. It is not enforced when a snapshot is loaded or deleted. An attacker can supply path separators, `..` components, or an absolute path. In Python, if the later operand supplied to `os.path.join()` is absolute, the preceding `SNAPSHOT_DIR` is discarded. The `.json` suffix limits the affected filenames to JSON paths, but it ...[truncated 2338 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Strictly validate snapshot identifiers before performing any filesystem operation: ```python import re SNAPSHOT_ID_RE = re.compile(r"^[0-9a-f]{8}$") def validate_snapshot_id(snap_id: str) -> str: if not SNAPSHOT_ID_RE.fullmatch(snap_id): raise ValueError("Invalid snapshot ID") return snap_id ``` 2. Resolve the candidate path and verify that it remains under the snapshot directory: ```python def snapshot_path(snap_id: str) -> str: validate_snapshot_id(snap_id) base = os.path.realpath(SNAPSHOT_DIR) candidate = os.path.realpath(os.path.join(base, f"{snap_id}.json")) if os.path.commonpath([base, candidate]) != base: raise ValueError("Snapshot path escapes snapshot directory") return candidate ``` 3. Use the validated helper consistently in `save_snapshot()`, `load_snapshot()`, and `delete_snapshot()`. 4. Track snapshots created by the current process or maintain a trusted index. Refuse to delete a file unless its identifier is present in that trusted state. 5. Reject symlinks where supported. Open files using secure flags such as `O_NOFOLLOW`, and verify the opened file is a regular file before reading or deleting it. 6. Validate the JSON schema after loading, including the types and permitted values of every field. 7. Run the utility under a dedicated, unprivileged account with write access only to its log and snapshot directories. 8. Add tests covering absolute paths, nested traversal, encoded or mixed path separators, symlinks, malformed IDs, and incompatible snapshot documents. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/auto_cost_report.py:39
Finding
Cost Reporting Processes All Main-Agent Session Records Without Least-Privilege Scoping<![CDATA[ ## Vulnerability Details **File Location**: `scripts/auto_cost_report.py`, lines 26 and 39–80; `scripts/task_logger.py`, lines 47 and 57–94 **Vulnerability Type**: Excessive access to sensitive Agent session records **Risk Level**: Medium ### Vulnerable Code From `scripts/auto_cost_report.py`: ```python SESSION_DIR = "/root/.openclaw/agents/main/sessions" ``` ```python def load_usage(date_strings): """Read all session files and return usage entries matching the given dates.""" date_set = set(date_strings) entries = [] if not os.path.exists(SESSION_DIR): return entries for fname in os.listdir(SESSION_DIR): if not fname.endswith(".jsonl"): continue fpath = os.path.join(SESSION_DIR, fname) session_id = fname.replace(".jsonl", "") try: with open(fpath) as f: for line in f: line = line.strip() if not line: continue try: obj = json.loads(line) ts = obj.get("timestamp", "") if not ts: ts = obj.get("message", {}).get("timestamp", "") if not ts or ts[:10] not in date_set: continue usage = obj.get("message", {}).get("usage", {}) if usage and "cost" in usage: entries.append({ "session_id": session_id[:8], "timestamp": ts, "date": ts[:10], "input": usage.get("input", 0), "output": usage.get("output", 0), "cache_read": usage.get("cacheRead", 0), "cache_write": usage.get("cacheWrite", 0), "cost": usage[ ...[truncated 4825 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace direct session-log parsing with an OpenClaw usage API or a dedicated usage-only ledger containing only: - Timestamp - Session identifier - Model identifier - Input and output token counts - Cache usage - Cost 2. Scope reporting to explicitly authorized session identifiers rather than enumerating every main-Agent session. 3. Make the session source configurable instead of hard-coding `/root/.openclaw/agents/main/sessions`. 4. Run the reporting scripts under a dedicated unprivileged account. Grant read access only to usage metadata and write access only to the required report directories. 5. If direct JSONL parsing is unavoidable, generate redacted usage-sidecar files at session-write time so reporting code never handles message content. 6. Enforce file ownership, restrictive permissions, and regular-file checks before opening session records. Avoid following symlinks. 7. Document the sensitive filesystem access clearly and require explicit operator approval before enabling automatic session scanning. 8. Add tests verifying that reporting cannot access sessions outside an authorized allowlist and that report output never contains conversation bodies or tool results. ]]>
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (9)

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
Describing a manual CLI-based logger as a real-time monitoring and guardrail system can cause operators to assume spend limits and reports are being enforced automatically when they are not. In a cost-management context, that gap is dangerous because failure is silent: nothing blocks excessive API usage unless the user remembers to invoke the scripts correctly.

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding
Describing a manual CLI-based logger as a real-time monitoring and guardrail system can cause operators to assume spend limits and reports are being enforced automatically when they are not. In a cost-management context, that gap is dangerous because failure is silent: nothing blocks excessive API usage unless the user remembers to invoke the scripts correctly.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
Describing a manual CLI-based logger as a real-time monitoring and guardrail system can cause operators to assume spend limits and reports are being enforced automatically when they are not. In a cost-management context, that gap is dangerous because failure is silent: nothing blocks excessive API usage unless the user remembers to invoke the scripts correctly.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
Describing a manual CLI-based logger as a real-time monitoring and guardrail system can cause operators to assume spend limits and reports are being enforced automatically when they are not. In a cost-management context, that gap is dangerous because failure is silent: nothing blocks excessive API usage unless the user remembers to invoke the scripts correctly.

External Model or Provider Selection

High
Category
Excessive Agency
Content
#!/usr/bin/env python3
"""
track_cost.py — Log a cost entry to the daily cost log.
Usage: python3 track_cost.py --task "email batch" --input 45000 --output 1200 --model claude-sonnet-4-6
"""
import json, argparse, os
from datetime import datetime, timezone
Confidence
90% confidence
Finding
Skill selects an external model or provider that may use a different account or billing plan than the operator expects. Undisclosed model switches can cause unexpected cost or quota consumption.

Lp3

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding
Without declared permissions the skill's intent is opaque and cannot be validated.

Vague Triggers

Medium
Confidence
90% confidence
Finding
The manifest description says to use the skill not only for specific actions like generating cost reports, but also broadly 'when a user asks about API costs, budget status, or why costs are high.' Those phrases are common conversational topics and the file does not provide exclusion conditions or tighter trigger boundaries, increasing the risk of unintended invocation.

Missing User Warnings

Medium
Confidence
85% confidence
Finding
The full report prints the raw task description field directly to stdout, which can expose sensitive operational content, customer data, prompts, or internal notes to anyone who can run the script or view its output. In a cost-monitoring skill, task descriptions are especially likely to contain business context, making accidental disclosure a realistic confidentiality risk even if there is no active attacker.

Natural-Language Policy Violations

Low
Confidence
77% confidence
Finding
The description mixes English instructions with Chinese text ('适用于一人公司和AI运营团队的实时成本监控工具。防止API成本失控。328+次安装。') but does not indicate that multilingual output is optional or user-selected. This can violate language/locale policy when a skill imposes a language choice without opt-in or justification.

Static analysis

No suspicious patterns detected.