Back to skill

Security audit

Task Watcher Skill

Security checks for vulnerabilities and agentic risk

Overview

The skill does what it says, but its background watcher accepts under-scoped local paths and task IDs that can cause unintended file reads, deletes, or writes.

Review before installing. Use this only in an environment where task records are written by trusted agents, run the watcher with a low-privilege account, and avoid enabling the cron job until task IDs and packet/status/notification paths are validated and confined to intended directories.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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/lib/notifiers.py:217
Finding
Path Traversal in Discord Notification Audit-File Creation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/lib/notifiers.py:217-228` **Vulnerability Type**: Path traversal and arbitrary file creation **Risk Level**: Medium ### Vulnerable Code ```python def _write_notification_file(self, task: CallbackTask, message: str) -> None: """Write notification to file as audit trail.""" notification_file = os.path.join( self.output_dir, task.task_id + "_" + task.updated_at.replace(':', '-') + ".json" ) notification_data = { 'task_id': task.task_id, 'channel': self.channel, 'reply_to': task.reply_to, 'message': message, 'timestamp': task.updated_at, } with open(notification_file, 'w', encoding='utf-8') as f: json.dump(notification_data, f, ensure_ascii=False, indent=2) ``` ### Technical Analysis The filename is constructed using `task.task_id` without validating or sanitizing path separators, absolute paths, `..` components, or symlink traversal. The task ID can originate from the `--task-id` argument in `scripts/register_task.py` or from a record inserted directly into the shared JSONL task store. Because `os.path.join()` does not guarantee that the resulting path remains under `self.output_dir`, a crafted task ID can escape the notification directory. An absolute task ID causes the output directory to be discarded, while traversal components can target writable parent directories. The watcher subsequently opens the computed destination in write mode, creating the file or truncating an existing file whose complete generated name can be predicted. The timestamp suffix limits direct selection of an arbitrary existing filename, but it does not prevent unauthorized file creation outside the intended directory. Existing directories must be present, and the effective impact remains constrained by the operating-system permissions of the watcher process. ### Attack Path 1. An attacker obtains the ability to register a task or ...[truncated 1324 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Validate task IDs at every ingestion boundary using a strict allowlist, for example: ```python import re if not re.fullmatch(r"[A-Za-z0-9_-]{1,128}", task.task_id): raise ValueError("Invalid task ID") ``` 2. Apply the same validation in `register_task.py`, `CallbackTask.__post_init__()`, and when deserializing records from the shared JSONL store. 3. Canonicalize and enforce the destination boundary before opening the file: ```python base = os.path.realpath(self.output_dir) destination = os.path.realpath(os.path.join(base, filename)) if os.path.commonpath([base, destination]) != base: raise ValueError("Notification path escapes output directory") ``` 4. Generate filenames independently of user-controlled identifiers, such as with a UUID, and store the original task ID only inside the JSON document. 5. Use atomic, exclusive file creation and reject symlinks where supported. Open files with restrictive permissions and avoid silently overwriting existing files. 6. Run the watcher as a dedicated, unprivileged account with write access limited to its task, audit, and notification directories. ]]>

T05 · Unauthorized Access and Privilege Escalation

Note
Location
scripts/lib/adapters.py:255
Finding
Unrestricted Local JSON File Read Through Task Packet Metadata<![CDATA[ ## Vulnerability Details **File Location**: `scripts/lib/adapters.py:255-261, 321-336` **Vulnerability Type**: Arbitrary local file read and trust-boundary violation **Risk Level**: Low ### Vulnerable Code ```python def _check_packet_file(self, task: CallbackTask) -> StateResult: """ Check state from one-click-posting packet file. Reads publish.status from packet JSON. Maps: draft->submitted, publishing->reviewing, published->approved """ # Ensure we use string value for state current_state = str(task.current_state.value if hasattr(task.current_state, 'value') else task.current_state) packet_path = task.metadata.get("packet_path") if task.metadata else None if not packet_path: return StateResult( state=current_state, terminal=False, confidence=0.0, source_of_truth="packet_file", error="No packet_path in task metadata" ) note_id = task.target_object_id return self._check_packet_at_path(packet_path, note_id) ``` ```python def _check_packet_at_path(self, packet_path: str, note_id: str) -> StateResult: """Check packet file at specific path.""" import os if not os.path.exists(packet_path): return StateResult( state="submitted", terminal=False, confidence=0.0, source_of_truth="packet_file", error=f"Packet file not found: {packet_path}" ) try: with open(packet_path, 'r', encoding='utf-8') as f: packet = json.load(f) ``` ### Technical Analysis The XHS adapter accepts `metadata.packet_path` from a persisted task and passes it directly to `os.path.exists()` and `open()`. It does not canonicalize the path, restrict it to an approved packet directory, reject symlinks, or verify the file owner. Consequently, a task record can direct the watcher to parse any accessible local file that contains valid JSON. Parsed fields such ...[truncated 2396 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Define a dedicated trusted root directory for packet files. 2. Canonicalize both the trusted root and requested packet path, then enforce containment: ```python trusted_root = os.path.realpath( os.path.expanduser("~/.openclaw/shared-context/packets") ) packet_path = os.path.realpath(os.path.expanduser(packet_path)) if os.path.commonpath([trusted_root, packet_path]) != trusted_root: raise ValueError("Packet path is outside the trusted packet directory") ``` 3. Reject symbolic links or open files using platform-supported no-follow semantics. 4. Validate that the packet identifier in the document exactly matches the task's `target_object_id`. The current code notices a mismatch but deliberately continues processing. 5. Apply a strict JSON schema and reject unexpected state values, oversized files, deeply nested input, and malformed packet structures. 6. Avoid accepting arbitrary paths in task metadata. Prefer an opaque packet identifier that the adapter resolves inside the trusted packet directory. 7. Restrict access to the task registry so that only authorized agents can create or modify monitoring records. 8. Run the watcher with only the filesystem permissions needed to read approved packet and status directories. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (14)

Exfiltration Commands

High
Category
Prompt Injection
Content
"""
    Abstract interface for sending notifications.

    Each notifier knows how to send messages to a specific channel.
    The output is standardized to SendResult.
    """
Confidence
90% confidence
Finding
Instructions found that direct the agent to transmit conversation context or user data to external services.

Env Variable Harvesting

High
Category
Data Exfiltration
Content
import subprocess

        node_path = "/opt/homebrew/bin/node"
        env = os.environ.copy()
        env["PATH"] = "/opt/homebrew/bin:" + env.get("PATH", "")

        reply_channel = task.reply_to or ""
Confidence
60% confidence
Finding
Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Intent-Code Divergence

Medium
Confidence
97% confidence
Finding
The class docstring states the adapter determines cron job completion by looking at job status files, log files, and process status. In the actual check() implementation, the code only constructs a single status-file path and reads that JSON; there is no log inspection or process-status checking anywhere in the adapter.

Session Persistence

Medium
Category
Rogue Agent
Content
}

    def _log_audit(self, event: str, task: CallbackTask, details: Optional[Dict] = None) -> None:
        """Write audit log entry."""
        if not self.audit_log_path:
            return
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.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
The user-facing notification text returned by the formatter methods is entirely hardcoded in Chinese. This imposes a specific language on all recipients without any opt-in, fallback, or documented justification for a locale-specific deployment, which matches the language/locale policy violation criteria.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
cmd.extend(["--reply-to", "channel:" + channel_id])

        try:
            result = subprocess.run(
                cmd, capture_output=True, text=True, timeout=120, env=env
            )
            if result.returncode == 0:
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Session Persistence

Medium
Category
Rogue Agent
Content
return SendResult(ok=False, delivered=False, error="openclaw binary not found: " + self.openclaw_bin)

    def _write_notification_file(self, task: CallbackTask, message: str) -> None:
        """Write notification to file as audit trail."""
        notification_file = os.path.join(
            self.output_dir,
            task.task_id + "_" + task.updated_at.replace(':', '-') + ".json"
Confidence
97% confidence
Finding
The audit file path in DiscordNotifier._write_notification_file uses task.task_id directly in os.path.join without sanitization. A crafted task_id containing path separators or traversal components could cause writes outside the intended notification directory, potentially overwriting arbitrary files writable by the process and persisting sensitive notification content in attacker-chosen locations.

Intent-Code Divergence

Medium
Confidence
96% confidence
Finding
DirectNotifier.read_notification and DirectNotifier.ack accept arbitrary file paths and perform open/remove operations without constraining them to the notifier's output directory. If an attacker can influence the filepath argument, they can read or delete unintended files accessible to the process, turning a notification helper into a local file access primitive.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
This code constructs user-facing notification messages entirely in Chinese string literals, including terminal-state summaries and recommended next actions. Because the file provides no user opt-in, language selection, or justification for a Chinese-only locale, it violates the language/locale policy for natural-language content.

Session Persistence

Medium
Category
Rogue Agent
Content
@pytest.fixture
    def temp_dir(self):
        """Create temporary directory for test data."""
        temp_dir = tempfile.mkdtemp()
        yield temp_dir
        shutil.rmtree(temp_dir)
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.

Natural-Language Policy Violations

Low
Confidence
89% confidence
Finding
This code file contains natural-language expectations that enforce Chinese output (for example, asserting the message contains Chinese text). Under the policy criteria, forcing a specific language without user opt-in can be a locale policy violation, and this test suggests the notifier behavior is expected to be Chinese-only.

Natural-Language Policy Violations

Low
Confidence
87% confidence
Finding
The assertion requires terminal notifications to include Chinese text, which indicates the skill behavior is tied to a single language. Because no opt-in, fallback, or documented region-specific justification appears in this file, this matches the natural-language locale policy concern.

Natural-Language Policy Violations

Low
Confidence
86% confidence
Finding
This assertion expects a Chinese phrase in the generated notification, indicating that at least one user-facing path is locked to a specific language. In the absence of documented opt-in or region-specific scope in this file, that is a policy-relevant locale constraint.

Natural-Language Policy Violations

Low
Confidence
86% confidence
Finding
The stored session message is expected to contain Chinese text, again implying user-facing notifications are forced into a single language. This is a natural-language policy issue unless the skill offers language choice or clearly documents a justified locale restriction.

Static analysis

No suspicious patterns detected.