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. ]]>
