Back to skill

Security audit

LLM Cost Tracker

Security checks for vulnerabilities and agentic risk

Overview

This cost tracker mostly matches its purpose, but review is warranted because a dry-run pruning command actually deletes data and normal reports automatically handle API credentials.

Install only if you are comfortable with the skill reading OpenClaw session history, creating a local usage database, and using an OpenRouter API key for account-summary data. Do not rely on scripts/prune_usage.py --dry-run until fixed, because it deletes rows. Prefer an explicit, least-privileged API key and avoid running setup or the tracker as root.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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

Warning
Location
scripts/run_tracker.py:85
Finding
Automatic API Credential Discovery Exposes the Secret in Process Arguments<![CDATA[ ## Vulnerability Details **File Location**: `scripts/run_tracker.py`, lines 85–128; invocation occurs at lines 416 and 431 **Vulnerability Type**: Automatic credential access and insecure secret handling **Risk Level**: Medium ### Vulnerable Code ```python def get_openrouter_api_key(): """ Resolve OpenRouter API key. Priority: env var > OpenClaw auth-profiles.json """ key = os.environ.get("OPENROUTER_API_KEY") if key: return key auth_candidates = [ os.path.expanduser("~/.openclaw/agents/main/agent/auth-profiles.json"), "/data/.openclaw/agents/main/agent/auth-profiles.json", "/root/.openclaw/agents/main/agent/auth-profiles.json", ] for auth_path in auth_candidates: if os.path.exists(auth_path): try: with open(auth_path) as f: data = json.load(f) key = data.get("profiles", {}).get("openrouter:default", {}).get("key") if key: return key except Exception: continue return None def get_openrouter_key_info(api_key): """ Fetch key info from /api/v1/auth/key. Returns dict with: usage, usage_daily, usage_weekly, usage_monthly, limit, limit_remaining. """ if not api_key: return None try: cmd = ["curl", "-s", "-H", f"Authorization: Bearer {api_key}", "https://openrouter.ai/api/v1/auth/key"] result = subprocess.run(cmd, capture_output=True, text=True, timeout=20) if result.returncode == 0: return json.loads(result.stdout).get("data", {}) except Exception: pass return None ``` The credential lookup and network request are invoked during ordinary report generation: ```python api_key = get_openrouter_api_key() ... key_info = get_openrouter_key_info(api_key) ``` ### Technical Analysis Every normal execution of `run_tracker.py` automatically searches the env ...[truncated 2219 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Make account-level OpenRouter enrichment explicitly opt-in, for example: ```bash python3 scripts/run_tracker.py --include-key-info ``` Local reports should not access credentials or the network unless that option is supplied. 2. Replace the external `curl` subprocess with an in-process HTTPS client. Set the authorization header through the client API so that the secret is not placed in child-process arguments. 3. Add a network-disable option and default it to disabled for scheduled and local database reports. 4. Avoid searching multiple privileged or unrelated account locations automatically. Prefer an explicitly configured credential source and validate file ownership and restrictive permissions before reading it. 5. Document clearly: - which credential is accessed; - which endpoint receives it; - when network access occurs; - which report fields depend on the remote request. 6. Ensure errors, debug output, telemetry, and exception logs never include authorization headers or the credential value. 7. Use a dedicated, least-privileged OpenRouter key if the remote account summary is enabled, and rotate any key suspected of exposure through process monitoring. ]]>

T08 · Insecure Dependencies

Note
Location
scripts/setup.py:35
Finding
Unbounded Dependency Installation Creates Supply-Chain Risk<![CDATA[ ## Vulnerability Details **File Location**: `requirements.txt`, line 1; `scripts/setup.py`, lines 35–38 **Vulnerability Type**: Unpinned third-party dependency installation **Risk Level**: Low ### Vulnerable Code `requirements.txt`: ```text tabulate>=0.9.0 ``` `scripts/setup.py`: ```python # Install deps if REQUIREMENTS.is_file(): result = subprocess.run([sys.executable, "-m", "pip", "install", "-r", str(REQUIREMENTS)], capture_output=True, text=True) ``` ### Technical Analysis The dependency declaration accepts any `tabulate` release equal to or newer than version 0.9.0. No upper bound, exact version, lock file, or package-integrity hash is supplied. The setup script passes this requirement directly to `pip`, allowing dependency resolution to change over time without a corresponding review of the Skill. There is no evidence that `tabulate` is a typosquatted package or that the configured package source is currently malicious. The risk arises because future installations automatically trust future releases and the active Python package index configuration. If an accepted release or package source is compromised, package installation or subsequent import could execute unreviewed code. ### Attack Path 1. A future accepted dependency release or configured package source is compromised, or an unsafe release is published. 2. A user runs `python3 scripts/setup.py` or follows the documented `pip install` command. 3. `pip` resolves the unconstrained requirement to the affected release. 4. Installation-time behavior or later package import executes the introduced code with the privileges of the user running setup or the tracker. 5. The malicious package gains access to files, environment variables, and network capabilities available to that user. ### Impact Assessment Impact is bounded by the privileges of the account running `pip` or executing the tracker. In a normal user installation, a compromised dependency could a ...[truncated 339 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin the dependency to a specifically reviewed version rather than accepting every future release: ```text tabulate==<reviewed-version> ``` 2. Generate and enforce cryptographic hashes, such as with a hash-locked requirements file and: ```bash pip install --require-hashes -r requirements.txt ``` 3. Maintain a lock file through a reproducible dependency-management tool and review dependency updates before changing the lock. 4. Install dependencies inside an isolated virtual environment instead of the system Python environment. 5. Avoid running setup as root unless strictly necessary. 6. Configure an explicitly trusted package index and prevent unreviewed environment-level `pip` configuration from silently redirecting package resolution. 7. Add automated dependency vulnerability and integrity checks to the release process. ]]>
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
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (27)

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
If the skill also installs dependencies with pip, performs setup validation, or checks the filesystem beyond cost reporting, that is a meaningful behavior mismatch from a user-facing reporting tool. Unexpected installation or setup actions can modify the host environment and expand attack surface, particularly when triggered through an automation framework.

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding
If the skill also installs dependencies with pip, performs setup validation, or checks the filesystem beyond cost reporting, that is a meaningful behavior mismatch from a user-facing reporting tool. Unexpected installation or setup actions can modify the host environment and expand attack surface, particularly when triggered through an automation framework.

Intent-Code Divergence

High
Confidence
99% confidence
Finding
The script performs deletion unconditionally by calling delete_before(conn, cutoff_iso) before checking args.dry_run, so '--dry-run' still permanently removes records. This is dangerous because operators may trust dry-run mode for impact assessment and accidentally destroy historical usage/accounting data, undermining auditability and cost reporting.

Missing User Warnings

High
Confidence
99% confidence
Finding
The printed '[DRY RUN]' message is misleading because the rows have already been deleted earlier in execution. In a cost-tracking skill, losing request_facts records directly corrupts historical reporting, reconciliation, and forensic analysis, making this more dangerous than a generic logging bug.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The markdown describes `prune_usage.py` as deleting old rows and shows a `--vacuum` example that performs deletion, but it does not explicitly warn that this is destructive and may permanently remove historical usage data. For markdown files, user-facing documentation should disclose behaviors that can affect user data or system integrity.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill declares executable behaviors involving environment access, file reads, network access, and shell execution, but does not declare any explicit tool scope or permission boundaries. This increases the risk of over-privileged execution because an agent may run the skill with broader capabilities than users expect, especially since the skill reads credentials and executes scripts.

Vague Triggers

Medium
Confidence
93% confidence
Finding
The phrase "collect usage data" is ambiguous and especially risky because the documentation says it silently populates a database and should not send chat output. Ambiguous silent triggers are dangerous in automation contexts because they can initiate hidden state changes without clear operator intent or visibility.

Vague Triggers

Medium
Confidence
91% confidence
Finding
The phrase "collect usage data" is ambiguous and especially risky because the documentation says it silently populates a database and should not send chat output. Ambiguous silent triggers are dangerous in automation contexts because they can initiate hidden state changes without clear operator intent or visibility.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The skill explicitly describes silent collection that populates a database but does not prominently warn that local state will be modified. Hidden or quiet state mutation is dangerous because users may believe they are requesting a read-only report when the skill is actually writing or updating persisted data.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
This code stores raw usage JSON and raw response metadata in an append-only database, and the script later writes those records to disk. While the module docstring says it collects request facts, it does not clearly warn users that request-derived metadata will be persistently stored locally, which is a data-handling behavior relevant to privacy and system state.

Dynamic import via __import__()

Medium
Category
Dangerous Code Execution
Content
import sqlite3
from datetime import datetime, timezone, timedelta

SKILL_DIR = __import__("os").path.dirname(__import__("os").path.dirname(__import__("os").path.abspath(__file__)))
DB_PATH = __import__("os").path.join(SKILL_DIR, "config", "usage.db")
Confidence
75% confidence
Finding
Dynamic __import__() can load arbitrary modules at runtime, bypassing static analysis and potentially importing malicious code.

Dynamic import via __import__()

Medium
Category
Dangerous Code Execution
Content
from datetime import datetime, timezone, timedelta

SKILL_DIR = __import__("os").path.dirname(__import__("os").path.dirname(__import__("os").path.abspath(__file__)))
DB_PATH = __import__("os").path.join(SKILL_DIR, "config", "usage.db")


def delete_before(conn, cutoff_iso):
Confidence
75% confidence
Finding
Dynamic __import__() can load arbitrary modules at runtime, bypassing static analysis and potentially importing malicious code.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The manifest describes this skill as tracking OpenRouter token usage/cost, generating reports, and silently collecting usage data. This file implements deletion of historical rows from the usage database, which is a data-retention/destructive maintenance operation not mentioned in the manifest's stated behavior.

Context-Inappropriate Capability

Medium
Confidence
91% confidence
Finding
For a skill framed around cost tracking, reporting windows, and usage collection, a CLI capability to permanently delete rows and vacuum the database is an additional administrative function. That maintenance capability is not an obvious requirement of generating reports or collecting usage data as described in the manifest.

Session Persistence

Medium
Category
Rogue Agent
Content
# ─── Schema (kept in sync with collect_usage.py) ─────────────────────────
SCHEMA = """
CREATE TABLE IF NOT EXISTS request_facts (
    id                      INTEGER PRIMARY KEY AUTOINCREMENT,
    openrouter_request_id   TEXT UNIQUE NOT NULL,
    created_at_utc          TEXT NOT NULL,
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
# ─── Schema (kept in sync with collect_usage.py) ─────────────────────────
SCHEMA = """
CREATE TABLE IF NOT EXISTS request_facts (
    id                      INTEGER PRIMARY KEY AUTOINCREMENT,
    openrouter_request_id   TEXT UNIQUE NOT NULL,
    created_at_utc          TEXT NOT NULL,
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.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The manifest describes this entrypoint as producing LLM cost/token reports, which implies read-oriented analytics over previously collected data. However, get_conn() executes CREATE TABLE and CREATE INDEX statements and enables WAL mode, causing the reporting path to modify the SQLite database on startup rather than only reading from it.

Context-Inappropriate Capability

Medium
Confidence
94% confidence
Finding
The script automatically reads OpenRouter API credentials from multiple filesystem locations, including user and root OpenClaw auth profiles, without explicit user consent at runtime. In an agent-skill context, this broad secret discovery increases the blast radius: a seemingly harmless reporting action can silently access credentials outside the skill's own configuration boundary and use them over the network.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
try:
        cmd = ["curl", "-s", "-H", f"Authorization: Bearer {api_key}",
               "https://openrouter.ai/api/v1/auth/key"]
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=20)
        if result.returncode == 0:
            return json.loads(result.stdout).get("data", {})
    except Exception:
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Context-Inappropriate Capability

Medium
Confidence
89% confidence
Finding
The debug mode prints raw request identifiers and stored raw usage payloads, which may contain sensitive metadata and could expose account activity, model usage details, or identifiers useful for correlation. In an agent environment, debug output may be forwarded to chat, logs, or external observers, making this disclosure more dangerous than a local developer-only script.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
# Install deps
    if REQUIREMENTS.is_file():
        result = subprocess.run([sys.executable, "-m", "pip", "install", "-r", str(REQUIREMENTS)],
                             capture_output=True, text=True)
        if result.returncode == 0:
            print("[OK] Dependencies installed")
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Natural-Language Policy Violations

Low
Confidence
84% confidence
Finding
The configuration and cron examples hard-code `Asia/Hong_Kong` / HKT +8 as the default timezone, which is a locale-specific assumption presented as the normal default behavior. The file does not offer an explicit user choice or explain why this locale constraint is required for a region-specific use case.

Missing User Warnings

Low
Confidence
88% confidence
Finding
Auto-detecting an API key from a local auth profile or environment variable without an explicit credential-access warning can surprise users and causes the skill to touch sensitive secrets as part of normal execution. While credential discovery is common for integrations, it should be transparent because it expands the sensitivity of the operation and the consequences of mis-triggering.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
The config hard-codes the timezone to "Asia/Hong_Kong" with a matching UTC offset, which imposes a specific locale setting. Under the policy, locale constraints should either offer user choice or be clearly documented and justified as region-specific.

Unpinned Dependencies

Low
Category
Supply Chain
Content
tabulate>=0.9.0
Confidence
91% confidence
Finding
The dependency is specified with a lower-bound only constraint (`tabulate>=0.9.0`), which allows installation of any future version, including versions that may introduce breaking changes or vulnerable transitive behavior. While `tabulate` is a common low-risk library, unpinned dependencies reduce build reproducibility and can expose the skill to supply-chain risk if a compromised or insecure release is later published.

Static analysis

Detected: suspicious.exposed_secret_literal

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
scripts/run_tracker.py:416