Back to skill

Security audit

OpenClaw WhatsApp GIF

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its WhatsApp GIF purpose, but it can send external messages without an explicit confirmation step and has unsafe media download and temp-file handling that warrants review.

Review this before installing if the agent can send WhatsApp messages automatically. Prefer requiring explicit user confirmation and recipient verification before any send, keep telemetry and remote URL fallback disabled, and avoid enabling web-scrape fallback unless you accept the weaker sourcing path. The downloader and temp cache should be hardened before use in shared or higher-risk environments.

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/send_gif.py:84
Finding
Redirect Allowlist Bypass and Unbounded Media Response Buffering<![CDATA[ ## Vulnerability Details **File Location**: `scripts/send_gif.py`, lines 84-98 **Vulnerability Type**: Redirect validation bypass and denial of service through unbounded response buffering **Risk Level**: Medium ### Vulnerable Code ```python def download_media(url: str, out_dir: Path, min_bytes: int, max_bytes: int, allowed_hosts, retries: int = 3): if not is_allowed_host(url, allowed_hosts): raise ValueError("url host is not allowed by policy") out_dir.mkdir(parents=True, exist_ok=True) last = None for i in range(retries): try: req = urllib.request.Request(url, headers={"User-Agent": "openclaw-whatsapp-gif/1.5"}) with urllib.request.urlopen(req, timeout=20) as resp: data = resp.read() content_type = resp.headers.get("Content-Type", "") if len(data) < min_bytes: raise ValueError(f"media too small ({len(data)} bytes)") if len(data) > max_bytes: raise ValueError(f"media too large ({len(data)} bytes)") ext = infer_extension(url, content_type) if ext not in {".mp4", ".gif", ".webm"}: raise ValueError(f"unsupported content type: {content_type or 'unknown'}") ``` ### Technical Analysis The function validates only the hostname in the initial candidate URL. Python's `urllib.request.urlopen` follows HTTP redirects automatically, but the code does not validate `resp.geturl()` or otherwise confirm that the final response still belongs to an allowed host. Consequently, an approved media URL that redirects can cross the configured network trust boundary. This is relevant if an approved provider is compromised, returns an attacker-influenced redirect, or exposes an open-redirect behavior. The configured `maxBytes` limit also does not impose an actual download or memory limit. `resp.read()` buffers the entire response in process memory before `len(data)` is compared with `max_byte ...[truncated 1764 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Validate the final response URL: - Disable automatic redirects and handle each redirect explicitly. - Validate every redirect destination against `allowedMediaHosts`. - After opening a response, verify `resp.geturl()` before reading its body. - Permit only HTTPS and reject embedded credentials, nonstandard schemes, and unexpected ports. 2. Stream downloads with a hard limit: - Read in bounded chunks rather than calling `resp.read()` without a size. - Abort immediately once the accumulated size exceeds `maxBytes`. - Reject a declared `Content-Length` greater than the configured limit, while still enforcing the streaming limit because the header may be absent or false. 3. Validate actual media content: - Require an allowlisted MIME type. - Inspect magic bytes or parse the media with a trusted decoder. - Ensure the detected file format agrees with the selected extension. - Reject HTML, JSON, text, and polyglot content. 4. Add tests covering: - Redirects from an approved host to an unapproved host. - Redirect chains. - Oversized chunked responses. - Incorrect MIME types and forged media extensions. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/send_gif.py:51
Finding
Predictable Shared Temporary Files Permit Symlink-Based File Overwrite<![CDATA[ ## Vulnerability Details **File Location**: `scripts/send_gif.py`, lines 51-52 and 84-100 **Vulnerability Type**: Unsafe temporary-file creation and symlink following **Risk Level**: Medium ### Vulnerable Code ```python def default_cache_dir() -> str: return str(Path(tempfile.gettempdir()) / "openclaw-whatsapp-gif") ``` ```python def download_media(url: str, out_dir: Path, min_bytes: int, max_bytes: int, allowed_hosts, retries: int = 3): if not is_allowed_host(url, allowed_hosts): raise ValueError("url host is not allowed by policy") out_dir.mkdir(parents=True, exist_ok=True) last = None for i in range(retries): try: req = urllib.request.Request(url, headers={"User-Agent": "openclaw-whatsapp-gif/1.5"}) with urllib.request.urlopen(req, timeout=20) as resp: data = resp.read() content_type = resp.headers.get("Content-Type", "") if len(data) < min_bytes: raise ValueError(f"media too small ({len(data)} bytes)") if len(data) > max_bytes: raise ValueError(f"media too large ({len(data)} bytes)") ext = infer_extension(url, content_type) if ext not in {".mp4", ".gif", ".webm"}: raise ValueError(f"unsupported content type: {content_type or 'unknown'}") out_path = out_dir / f"gif_{hashlib.sha1(url.encode()).hexdigest()[:12]}{ext}" out_path.write_bytes(data) ``` ### Technical Analysis The default cache path is a fixed directory under the operating system's shared temporary directory. Media filenames are deterministic because they are generated from the first 12 hexadecimal characters of the SHA-1 hash of a public media URL. The implementation calls `mkdir(..., exist_ok=True)` without checking whether the existing path is owned by the current user, is a real directory rather than a link, or has secure permissions. It then uses `Path.write_bytes`, which ...[truncated 2123 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Create a private cache directory: - Use `tempfile.mkdtemp()` for each run, or create a per-user directory with mode `0700`. - Verify that an existing cache path is a real directory, is owned by the current user, and is not writable by other users. - Reject symbolic links in every parent component where supported. 2. Create files securely: - Open destination files with `os.open` using `O_CREAT | O_EXCL | O_WRONLY`. - Add `O_NOFOLLOW` on platforms that support it. - Use restrictive permissions such as `0600`. - Write through the securely opened file descriptor instead of reopening the pathname with `Path.write_bytes`. 3. Avoid deterministic shared filenames: - Use `tempfile.NamedTemporaryFile` or a cryptographically random filename. - If URL-based deduplication is required, keep the mapping in a private directory and still use exclusive creation. 4. Harden lifecycle management: - Remove temporary files after delivery when they are no longer needed. - Do not accept an arbitrary insecure cache directory without validating its ownership and permissions. - Add tests that pre-create the cache directory, destination symlinks, and colliding files to verify safe failure. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (15)

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding
The skill claims a narrowly scoped, safe, deterministic GIF workflow, but it also supports telemetry logging of user queries/media URLs, remote URL delivery/fallback, and generic target-based payload generation. In context, this is more dangerous because the skill operates in a messaging environment where user intent, recipient identifiers, and media URLs may be sensitive, and hidden logging or broader delivery modes can lead to privacy leakage or misuse.

Tp4

High
Category
MCP Tool Poisoning
Confidence
88% confidence
Finding
The skill claims a narrowly scoped, safe, deterministic GIF workflow, but it also supports telemetry logging of user queries/media URLs, remote URL delivery/fallback, and generic target-based payload generation. In context, this is more dangerous because the skill operates in a messaging environment where user intent, recipient identifiers, and media URLs may be sensitive, and hidden logging or broader delivery modes can lead to privacy leakage or misuse.

Lp3

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding
The skill invokes shell scripts, accesses environment variables, performs network fetches, and writes to temporary storage, but it does not declare any explicit tool scope or permissions. This weakens policy enforcement and reviewability because a host agent may grant broader capabilities than users or platform controls expect, especially for a skill that can download media and prepare outbound WhatsApp messages.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The skill description and default prompt emphasize sending a GIF in WhatsApp but contain no requirement for user confirmation, preview, or warning before performing the outbound message action. Because sending a message is a side effect in an external communication channel, lack of explicit confirmation can lead to accidental or unauthorized sends, reputational harm, or misuse if invoked in the wrong context.

Vague Triggers

Medium
Confidence
88% confidence
Finding
The default prompt and implicit invocation allow this skill to be triggered by broad, common requests for GIFs in WhatsApp, which can cause the agent to take an external action without clear user intent specific to this skill. In a messaging context, ambiguous auto-invocation increases the chance of unintended content sourcing or sending to the wrong conversation, making this a real safety and security concern rather than a purely cosmetic prompt issue.

External Transmission

Medium
Category
Data Exfiltration
Content
return []
    params = {"api_key": key, "q": compact_query(expand_query(query)), "limit": str(limit), "rating": "g", "lang": "en"}
    try:
        data = http_get_json("https://api.giphy.com/v1/gifs/search?" + urllib.parse.urlencode(params))
    except Exception:
        return []
    out = []
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
return []
    params = {"api_key": key, "q": compact_query(expand_query(query)), "limit": str(limit), "rating": "g", "lang": "en"}
    try:
        data = http_get_json("https://api.giphy.com/v1/gifs/search?" + urllib.parse.urlencode(params))
    except Exception:
        return []
    out = []
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Description-Behavior Mismatch

Medium
Confidence
91% confidence
Finding
The optional `search_tenor_web` path scrapes Tenor HTML and extracts media URLs via regex, bypassing the stronger structure and metadata constraints provided by official APIs. In this skill's context, that weakens the claimed deterministic/safe sourcing model and can increase the chance of unstable behavior, unexpected content selection, or policy bypass if the page structure or embedded URLs change.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def run(cmd):
    return subprocess.check_output(cmd, text=True)


def main():
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def run_find_gif(script_dir: Path, query: str, limit: int):
    py = sys.executable or "python3"
    cmd = [py, str(script_dir / "find_gif.py"), query, "--limit", str(limit), "--json"]
    out = subprocess.check_output(cmd, text=True)
    return json.loads(out)
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
When telemetry is enabled, the script appends the raw user query, selected URL, errors, and mode to a temp-directory log file without any in-code notice, minimization, or retention controls. User queries in messaging contexts can contain sensitive personal data, and writing them to shared or weakly protected local storage can create an unintended privacy leak.

Missing User Warnings

Low
Confidence
84% confidence
Finding
This code sends the user's query to external services (Tenor and Giphy) and uses sensitive environment variables for authentication, but there is no confirmation prompt, user-facing log/print, or explanatory comment/docstring warning that external providers will receive the query. For a code file, these outbound data transmissions and credential use meet the missing-warning criteria.

Context-Inappropriate Capability

Low
Confidence
78% confidence
Finding
The stated purpose is to find and send reaction GIFs in WhatsApp chats; credential access is an implementation capability not justified by the manifest text itself. While common for API-backed integrations, this still expands the skill's operational capability beyond the user-facing purpose description.

Context-Inappropriate Capability

Low
Confidence
78% confidence
Finding
Accessing environment variables for API keys is a capability not evident from the manifest's narrow description of selecting and sending GIF reactions. This is likely an implementation detail, but the manifest does not disclose direct secret access by the skill.

Natural-Language Policy Violations

Low
Confidence
96% confidence
Finding
The request hard-codes the Giphy API language parameter to "en", which imposes a specific language/locale without any user choice or opt-in. This matches the natural-language policy violation category for forced language settings.

Static analysis

No suspicious patterns detected.