Back to skill

Security audit

miab-observer

Security checks for vulnerabilities and agentic risk

Overview

The skill is mostly coherent, but its optional Discord notifier can automatically send ledger task/result content externally with weak state safety and no mention neutralization.

Review before installing if your MIAB ledger may contain secrets, internal prompts, customer data, or untrusted agent output. Use the local observer by default, configure CLAW_CLOSED_TARGET only for an approved destination, run the dry-run script first, and avoid unattended Discord delivery until mention escaping, redaction, and fail-closed notifier state handling are added.

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 (2)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/notify_closed_bottles.py:126
Finding

Malformed notifier state or ledger truncation can replay previously delivered messages

Content
View full analysis
dict: p = state_file() p.parent.mkdir(parents=True, exist_ok=True) if p.exists(): try: return json.loads(p.read_text(encoding="utf-8")) except Exception: pass return {"enabled": True, "last_processed_line": 0} ``` The processing path also automatically rewinds an out-of-range cursor: ```python lines = load_all_lines() total = len(lines) cursor = state.get("last_processed_line", 0) if cursor > total: cursor = 0 ``` ### Technical Analysis The notifier treats a missing state file and an existing but malformed state file identically. If JSON parsing fails, the broad exception handler silently discards the failure and returns a fresh state with the notifier enabled and its cursor set to zero. This also discards the persisted `delivered_ids` deduplication list. The same replay behavior occurs when the saved cursor exceeds the current number of non-empty ledger lines. Instead of failing closed and requiring operator review, the code resets the cursor to zero. The observer implementation in `miab_observer.py` already demonstrates the safer approach by rejecting malformed state rather than rewinding. The closed-bottle notifier does not apply that protection even though its output is externally delivered to Discord. ### Attack Path 1. An attacker or faulty local process with write access corrupts or truncates `closed_bottle_state.json`, causing JSON parsing to fail. Alternatively, the attacker truncates or replaces the ledger so that the stored cursor exceeds its current line count. 2. A scheduled job subsequently invokes `notify_closed_bottles.py process`. 3. `load_state()` sil ...[truncated 819 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/notify_closed_bottles.py:144
Finding

Untrusted ledger content is delivered to Discord without neutralizing mentions

Content
View full analysis
limit: return cleaned[: limit - 3] + "..." return cleaned ``` Ledger-controlled task, result, and reason values are incorporated into the outgoing message: ```python lines = [f"{icon} [MIAB Bottle {label}] {cid}"] if task: lines.append(f" Task: {task}") if end_event == "resolve" and end_rec.get("result"): lines.append(f" Result: {sanitize(end_rec.get('result'), limit=400)}") if end_event == "cancel" and end_rec.get("reason"): lines.append(f" Reason: {end_rec.get('reason')}") if end_event == "fail" and end_rec.get("reason"): lines.append(f" Reason: {end_rec.get('reason')}") ``` The resulting text is delivered directly to Discord: ```python def deliver_one(message: str) -> bool: """Deterministic delivery: shell out to `openclaw message send`. No LLM in this path. Returns True on success.""" cmd = ["openclaw", "message", "send", "--channel", "discord", "--target", target_channel(), "--message", message] acct = account_id() if acct: cmd += ["--account", acct] try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) ``` ### Technical Analysis The ledger is treated as trusted presentation content even though task descriptions, results, reasons, agent labels, and related fields may originate from other agents or processes. The `sanitize() ...[truncated 1890 chars]
Remediation
View remediation
`, `<@&...>`, and `<#...>`. - Apply the same sanitizer to every ledger-controlled field, particularly cancellation and failure reasons and history entries. - Prefer a transport option that disables allowed mentions entirely. Configure the Discord payload with an empty allowed-mentions policy if `openclaw message send` supports it. - Remove or clearly delimit untrusted links, or apply an allowlist when clickable links are not required. - Enforce maximum lengths both per field and for the final message. - Add tests covering mass mentions, role mentions, user mentions, control characters, multiline content, Markdown formatting, and oversized values. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Memory PoisoningPersistent Context Injection, Context Window Stuffing, Memory Manipulation
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (4)

Memory Manipulation

High
Category
Memory Poisoning
Confidence
90% confidence
Finding

Skill manipulates agent memory, state, or stored context. Memory corruption can alter personality, override safety rules, or cause unpredictable behavior.

Content

Scanner excerpt · CHANGELOG.md (reported line 69)May include surrounding context.

md
### Fixed

- **A corrupt state file no longer replays the entire ledger.** `load_state()` swallowed every
  exception and fell through to the same `{"enabled": false, "last_processed_line": 0}` default
  it uses for a genuine fresh start. A truncated or malformed `queue_state.json` therefore
  rewound the cursor to 0 without a word, and the next `process` wrote every ledger record that

Session Persistence

Medium
Category
Rogue Agent
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.

Content

Scanner excerpt · scripts/miab_observer.py (reported line 7)May include surrounding context.

python
PREREQUISITE: Requires the `miab-broker` skill to be installed and active.
It tails the append-only callback ledger (state/callbacks/ledger.jsonl) managed by miab-broker,
converts raw create / forward / return / resolve / cancel / fail / corrupt events into
human-readable log entries using the agent identity map, advances a once-only cursor, and — when the live
toggle is on — writes the formatted batch to the log file ($CLAW_HOME/logs/interagent-queue.log).

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The script reconstructs messages from ledger fields such as task, result, and reason and sends them to Discord automatically. If the ledger can contain secrets, internal prompts, user data, or attacker-injected content, this creates an exfiltration path to an external channel without content filtering, redaction, or recipient verification.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/notify_closed_bottles.py (reported line 304)May include surrounding context.

python
if acct:
        cmd += ["--account", acct]
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
        if result.returncode != 0:
            print(f"delivery failed (exit {result.returncode}): {result.stderr.strip()}", file=sys.stderr)
            return False