Back to skill

Security audit

Clawhub Rate Limited Publisher Fixed

Security checks for vulnerabilities and agentic risk

Overview

This skill has a coherent publishing purpose, but its helper script turns queue entries into broad shell commands that can run more than just ClawHub publishing.

Review before installing. Use only queue files you created and trust, avoid the optional command field, and do not schedule this helper until the uploader is changed to call a fixed argument list such as clawhub publish <path> with shell=False and protected state updates.

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

Error
Location
scripts/clawhub_rate_limited_uploader.py:56
Finding
Arbitrary Command Execution Through Unsafe Queue Command Processing<![CDATA[ ## Vulnerability Details **File Location**: `scripts/clawhub_rate_limited_uploader.py`, lines 56–110 **Vulnerability Type**: OS command injection caused by attacker-controlled command templates and `shell=True` **Risk Level**: High ### Vulnerable Code ```python def normalize_items(queue_data: Dict[str, Any]) -> List[QueueItem]: items = queue_data.get("items") if not isinstance(items, list): raise SystemExit('Queue JSON must contain an "items" array.') normalized: List[QueueItem] = [] for index, item in enumerate(items): if not isinstance(item, dict): raise SystemExit(f"Queue item #{index} must be an object.") path = item.get("path") if not isinstance(path, str) or not path.strip(): raise SystemExit(f'Queue item #{index} missing non-empty "path".') command = item.get("command", DEFAULT_COMMAND) if not isinstance(command, str) or "{path}" not in command: raise SystemExit(f'Queue item #{index} has invalid "command"; it must be a string containing "{{path}}".') normalized.append(QueueItem(path=path, command=command)) return normalized def ensure_skill_dir(path_str: str) -> Path: path = Path(path_str).expanduser().resolve() if not path.exists(): raise SystemExit(f"Skill path does not exist: {path}") if not path.is_dir(): raise SystemExit(f"Skill path is not a directory: {path}") if not (path / "SKILL.md").exists(): raise SystemExit(f"Skill directory does not contain SKILL.md: {path}") return path def run_publish(item: QueueItem, execute: bool) -> subprocess.CompletedProcess[str] | None: skill_path = ensure_skill_dir(item.path) command_str = item.command.format(path=str(skill_path)) print(f"[info] command: {command_str}") if not execute: return None return subprocess.run( command_str, shell=True, text=True, capture_output=True, chec ...[truncated 3196 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Remove shell command-string execution and invoke the ClawHub executable with a fixed argument vector: ```python def run_publish(item: QueueItem, execute: bool) -> subprocess.CompletedProcess[str] | None: skill_path = ensure_skill_dir(item.path) command = ["clawhub", "publish", str(skill_path)] print(f"[info] command: {shlex.join(command)}") if not execute: return None return subprocess.run( command, shell=False, text=True, capture_output=True, check=False, cwd=str(skill_path.parent), ) ``` Additional hardening should include: 1. Remove the `command` property from the queue schema if the only supported operation is publishing. 2. If customization is essential, represent commands as JSON argument arrays rather than shell strings. 3. Strictly allowlist the executable and operation, such as exactly `clawhub` followed by `publish`. 4. Never pass queue-controlled input to `shell=True`. 5. Treat displayed commands as logs only; use `shlex.join()` for readable output rather than for execution. 6. Reject unexpected queue fields or document and validate a strict schema. 7. Add tests using paths and command values containing quotes, semicolons, substitutions, spaces, and redirection characters to confirm they remain single literal arguments. 8. Restrict queue-file ownership and permissions as defense in depth, while not relying on permissions as a replacement for safe process invocation. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/clawhub_rate_limited_uploader.py:126
Finding
Rolling Rate Limit Can Be Bypassed by Concurrent Uploader Processes<![CDATA[ ## Vulnerability Details **File Location**: `scripts/clawhub_rate_limited_uploader.py`, lines 126–170 **Vulnerability Type**: Race condition and non-atomic rate-limit state management **Risk Level**: Medium ### Vulnerable Code ```python state: Dict[str, Any] = {"attempts": [], "statuses": {}} if state_path.exists(): state = load_json(state_path) if not isinstance(state, dict): raise SystemExit(f"State file must be a JSON object: {state_path}") state.setdefault("attempts", []) state.setdefault("statuses", {}) now = time.time() state["attempts"] = prune_attempts(state.get("attempts", []), now) remaining = MAX_PER_HOUR - len(state["attempts"]) print(f"[info] rolling-window attempts in last hour: {len(state['attempts'])}/{MAX_PER_HOUR}") if remaining <= 0: earliest = min(float(a.get("ts", now)) for a in state["attempts"]) wait_seconds = int((earliest + WINDOW_SECONDS) - now) print(f"[warn] hourly cap reached; next slot in about {max(wait_seconds, 0)} seconds") save_json(state_path, state) return 0 idx = next_pending_index(state, items) if idx is None: print("[info] queue complete; nothing pending") save_json(state_path, state) return 0 item = items[idx] try: skill_path = ensure_skill_dir(item.path) except SystemExit as exc: state["statuses"][str(idx)] = {"status": "failed", "reason": str(exc), "updatedAt": int(now)} save_json(state_path, state) print(f"[error] {exc}") return 2 print(f"[info] next skill: {skill_path}") result = run_publish(item, execute=args.execute) if args.dry_run: state["statuses"].setdefault(str(idx), {"status": "pending"}) save_json(state_path, state) print("[info] dry-run complete") return 0 attempt_record = {"ts": now, "index": idx, "path": str(skill_path)} state["attempts"].append ...[truncated 2878 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Implement cross-process synchronization and reserve quota before starting the external publication operation. A robust design should: 1. Acquire an exclusive lock associated with the state file before reading or changing rate-limit state. 2. Reload and validate the state only after acquiring the lock. 3. Prune expired records and atomically reserve an attempt slot before invoking `clawhub`. 4. Persist the reservation using a temporary file in the same directory, flush it, and replace the state file with an atomic rename. 5. Ensure that every reserved attempt counts toward the limit even if publication crashes or fails. 6. Prevent two processes from selecting the same pending item, for example by recording an `in_progress` state under the same lock. 7. After execution, reacquire the lock, reload current state, and update the matching reservation instead of overwriting state derived from a stale snapshot. 8. Define stale-reservation recovery for processes that terminate unexpectedly. 9. Apply restrictive permissions to the state file where appropriate. On Unix-like supported platforms, an exclusive advisory lock can be implemented with `fcntl.flock()`. A portable locking library may be used if additional operating systems are later supported. The lock protocol must be shared by every process using the state. Add an automated concurrency test that launches more than five `--execute` processes simultaneously against a mocked `clawhub` executable and verifies that no more than the configured number of attempts can start within the window. Also verify that the final state remains valid JSON and retains every reservation and result. ]]>
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (9)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The skill claims a constrained rate-limited ClawHub publishing workflow, but it also permits arbitrary per-item command templates containing {path}, effectively turning queue entries into shell execution inputs. That mismatch is dangerous because users may trust the skill as a narrowly scoped publisher while an attacker-controlled or malformed queue file can trigger execution of unintended commands under the host's local privileges.

Ae1

High
Category
analysis-evasion
Content
1. Verify the skill folder exists and contains `SKILL.md`.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Context-Inappropriate Capability

High
Confidence
98% confidence
Finding
The skill's stated purpose is rate-limited publication of local skills, but the implementation allows the queue to supply any templated shell command containing {path}. That design lets an attacker who can modify the queue file turn a publish workflow into a general-purpose command runner, expanding the capability far beyond the documented and expected boundary.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
print(f"[info] command: {command_str}")
    if not execute:
        return None
    return subprocess.run(
        command_str,
        shell=True,
        text=True,
Confidence
99% confidence
Finding
Using subprocess.run with shell=True on a string influenced by queue data is a classic command-injection sink. Even if {path} is populated from a validated directory path, the queue-controlled command template itself can embed additional shell operations, causing execution of unintended commands under the privileges of the publishing process.

Natural-Language Policy Violations

Medium
Confidence
94% confidence
Finding
The primary skill description is written in Chinese and the document does not provide an English alternative, user language selection, or any note that the skill is intentionally region-specific. This can violate language/locale policy because it imposes a specific language on users without opt-in.

Lp3

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding
The skill instructs the host to run a local Python helper, build/update a queue file, and use scheduler-driven execution, which implies shell, file read, and file write capabilities without declaring any explicit tool scope or permission boundaries. In practice, this can cause the agent or host integration to grant broader execution authority than reviewers expect, increasing the risk of unintended command execution or file modification.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The documentation and feature description suggest a safe, purpose-specific publisher, but the actual queue schema includes a free-form command field that can execute arbitrary shell commands. This mismatch increases operator trust and the chance of unsafe deployment because reviewers may assume the tool only publishes skills when it actually provides broader execution capability.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
print(f"[info] command: {command_str}")
    if not execute:
        return None
    return subprocess.run(
        command_str,
        shell=True,
        text=True,
Confidence
99% confidence
Finding
The script builds a shell command from queue-controlled data and executes it with shell=True, which allows arbitrary shell metacharacters or an entirely different command to run. Because the queue format explicitly permits a per-item command template, this is not limited to publishing and becomes arbitrary command execution on the host running the scheduler.

Natural-Language Policy Violations

Low
Confidence
93% confidence
Finding
This markdown file presents all substantive instructions and checklist items in Chinese, but does not indicate that the user can choose another language or that the locale is intentionally constrained. The policy requires flagging language or locale constraints when a specific language is effectively forced without user opt-in.

Static analysis

No suspicious patterns detected.