Back to skill

Security audit

Openclaw Ledger

Security checks for vulnerabilities and agentic risk

Overview

This local audit-ledger skill is mostly purpose-aligned, but it overstates its tamper-detection guarantees and includes under-documented commands that can export or overwrite ledger data.

Install only if you want a local workspace change ledger and are comfortable with .ledger storing file names, hashes, timestamps, and user messages. Do not rely on it as forensic-grade tamper proofing against someone who can write to the workspace. Review or avoid the protect, restore, and export commands unless you explicitly want ledger replacement or full ledger disclosure.

Vulnerability Patterns
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (1)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/ledger.py:113
Finding
Unauthenticated Hash Chain Fails to Detect Rewritten or Truncated Audit History<![CDATA[ ## Vulnerability Details **File Location**: `scripts/ledger.py:73-85`, `scripts/ledger.py:113-126`, and `scripts/ledger.py:194-204` **Vulnerability Type**: Missing authentication and trusted state anchoring for an audit log **Risk Level**: Medium ### Vulnerable Code ```python def get_last_hash(ws): cp = chain_path(ws) if not cp.exists(): return GENESIS_HASH last = "" try: with open(cp, "r", encoding="utf-8") as f: for line in f: if line.strip(): last = line.strip() except (OSError, PermissionError): return GENESIS_HASH return hash_entry(last) if last else GENESIS_HASH def append_entry(ws, event_type, data): prev = get_last_hash(ws) entry = {"timestamp": now_iso(), "prev_hash": prev, "event": event_type, "data": data} entry_json = json.dumps(entry, separators=(",", ":"), sort_keys=True) with open(chain_path(ws), "a", encoding="utf-8") as f: f.write(entry_json + "\n") return hash_entry(entry_json) ``` ```python def verify_chain_integrity(entries): """Returns (is_intact, broken_at, count).""" if not entries: return True, None, 0 expected = GENESIS_HASH for i, ej in enumerate(entries): try: e = json.loads(ej) except json.JSONDecodeError: return False, i, len(entries) if e.get("prev_hash") != expected: return False, i, len(entries) expected = hash_entry(ej) return True, None, len(entries) ``` ```python def cmd_verify(ws): cp = chain_path(ws) if not cp.exists(): print("No ledger found. Run 'init' first."); return 1 print("=" * 60) print("OPENCLAW LEDGER FULL -- CHAIN VERIFICATION") print("=" * 60 + "\n") entries = read_chain(ws) if not entries: print("[EMPTY] No entries in chain."); return 0 intact, broken, count = verify_chain_integrity(entries) if not intact: ej = entries[br ...[truncated 2718 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Authenticate every entry with a keyed HMAC or digital signature rather than relying only on an unkeyed SHA-256 chain. 2. Store the signing key outside the audited workspace and restrict it using operating-system permissions or a platform keystore. A key stored alongside the ledger would not prevent an attacker with workspace write access from forging history. 3. Persist the latest trusted head hash and expected entry count in an independent, access-controlled location. Verification should compare the calculated state against this trusted checkpoint. 4. For stronger protection, publish or periodically anchor signed head hashes in an external append-only service or another independently administered storage location. 5. Treat an unexpectedly empty chain, a reduced entry count, or a head that differs from the trusted checkpoint as tampering and return exit code `2`. 6. Authenticate frozen backups and their metadata. Store recovery copies outside the writable workspace or sign their head hashes so that they cannot be silently replaced. 7. Use atomic writes, file locking, and restrictive permissions for chain and session updates to reduce race conditions and unauthorized modification. 8. Update the documentation to clarify that an unanchored hash chain alone cannot detect a complete rewrite or valid-prefix truncation. ]]>
Vulnerability Patterns
  • 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
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (3)

Missing User Warnings

High
Confidence
94% confidence
Finding
The protect flow automatically overwrites the active ledger with a frozen backup via shutil.copy2(clean, cp) after detecting tampering, without interactive confirmation, dry-run mode, or an explicit opt-in flag. In an agent context, this is a destructive state-changing action that can erase the current chain state, destroy forensic context, or let an attacker influence recovery behavior if backup selection is manipulated.

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill is explicitly user-invocable and documents execution of a local Python script that reads workspace state, writes a tamper-evident ledger, and likely shells through the host runtime, yet it declares no permissions or allowed-tools scope. That creates an authorization gap: an agent platform may expose file, environment, or shell capabilities more broadly than intended, making it harder to enforce least privilege and increasing the chance of unsafe execution in sensitive workspaces.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
The export command prints the full workspace path and the entire ledger contents, including file names, timestamps, messages, and snapshots, directly to stdout with no redaction or privacy warning. In agent environments, stdout is often captured by logs, orchestration systems, or upstream tools, so this can unintentionally disclose sensitive repository structure, operational history, and possibly user-provided messages.

Static analysis

No suspicious patterns detected.