Back to skill

Security audit

GIF Multi

Security checks for vulnerabilities and agentic risk

Overview

This GIF skill is mostly purpose-aligned, but it uses under-scoped message-sending and cleanup behavior that can affect local files or execute unsafe shell cleanup if channel names are attacker-influenced.

Install only if you are comfortable with GIF searches being sent to Giphy, the skill sending GIFs on its own in natural mode, and local cache cleanup behavior. Prefer setting mode to `on_request` and avoid using untrusted or arbitrary channel identifiers until cleanup is changed to a non-shell file operation and the cache is isolated to the skill.

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
SKILL.md:45
Finding
Shell Command Injection Through an Unsafely Generated Cleanup Path<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:45`; `scripts/gif_multi.py:228-231`; `scripts/gif_multi.py:344-347` **Vulnerability Type**: Shell command injection through an attacker-influenced filename **Risk Level**: High ### Vulnerable Code ```markdown 5. **Clean up**: `exec(rm <path>)` after sending. ``` ```python # Unique per-channel filename with timestamp — no topic/thread collisions safe_ch = channel.replace("/", "_").replace(" ", "_") ts = int(time.time() * 1000) out_base = os.path.join(out_dir, f"gif_{safe_ch}_{ts}") ``` ```python gif_path = os.path.join(out_dir, f"source_{channel}.gif") urllib.request.urlretrieve(result["gif_url"], gif_path) conv = convert(gif_path, channel, out_dir) ``` ### Technical Analysis The `--channel` value contributes directly to generated filenames. The only sanitization applied to the converted output filename replaces forward slashes and spaces: ```python safe_ch = channel.replace("/", "_").replace(" ", "_") ``` This does not remove or reject shell metacharacters such as semicolons, command substitutions, backticks, quotes, redirection operators, or newline characters. The source GIF filename uses the channel value without even this limited sanitization. The script returns the resulting path to the Agent, while `SKILL.md` instructs the Agent to delete that path using the shell-oriented operation `exec(rm <path>)`. If the Agent interpolates the returned path into this command without robust argument separation or shell escaping, shell metacharacters embedded in the channel value can be interpreted as executable syntax. The use of `subprocess.run()` for FFmpeg does not itself introduce shell injection because it passes an argument list and does not enable `shell=True`. The vulnerable boundary is the documented cleanup operation performed after the script returns an attacker-influenced path. ### Attack Path 1. An attacker causes a crafted channel identifier to be passed to the script, directly th ...[truncated 1386 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Do not perform cleanup by interpolating a returned path into shell command text. 2. Delete the file through a filesystem API such as `os.remove()` inside the Python script after the send operation, or use a tool interface that passes the path as a distinct argument without invoking a shell. 3. If external cleanup is unavoidable, pass an argument array equivalent to `["rm", "--", path]` rather than constructing `rm <path>`. 4. Validate channel identifiers against a strict allowlist derived from `CHANNEL_PROFILES` or discovered channel IDs. 5. Generate cache filenames independently of user-controlled values, such as with `tempfile`, a UUID, or a cryptographically random token. 6. If a channel label must appear in a filename, permit only a narrow character set such as ASCII letters, digits, underscores, and hyphens. Reject rather than partially transform invalid identifiers. 7. Ensure the final resolved file path remains inside the expected cache directory before creating, returning, or deleting it. 8. Update `SKILL.md` to prescribe a non-shell cleanup mechanism. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/gif_multi.py:88
Finding
Over-Broad Deletion of Unrelated Files in a Shared Cache Directory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/gif_multi.py:88-97`; `scripts/gif_multi.py:339-342` **Vulnerability Type**: Unsafe temporary-file cleanup and insufficient cache isolation **Risk Level**: Medium ### Vulnerable Code ```python def _purge_old(out_dir, max_age=600): """Delete cached files older than max_age seconds (default 10 min).""" now = time.time() try: if os.path.exists(out_dir): for f in os.listdir(out_dir): fp = os.path.join(out_dir, f) if os.path.isfile(fp) and (now - os.path.getmtime(fp)) > max_age: os.remove(fp) except OSError: pass ``` ```python # Cache in workspace (accessible by message tool) out_dir = os.path.join(os.path.dirname(SKILL_DIR), "..", ".gif_cache") out_dir = os.path.abspath(out_dir) os.makedirs(out_dir, exist_ok=True) _purge_old(out_dir) # Only old files (>10 min), leave recent ones alone ``` ### Technical Analysis The cache path is constructed by moving above the Skill directory and then appending `.gif_cache`. For the audited installation layout, this resolves to `/tmp/.gif_cache`, which is outside the Skill directory and can function as a shared location. The cleanup routine enumerates every regular file in that directory and deletes any file older than ten minutes. It does not verify that a file: - Was created by this Skill. - Has an expected GIF or MP4 filename. - Uses a Skill-specific prefix. - Is recorded in Skill-owned state. - Belongs to the expected user or process. - Resolves to a Skill-private cache location. Consequently, the cleanup behavior exceeds the minimum filesystem access necessary to convert and send GIFs. It can delete unrelated files placed in the same directory by other Skill instances, processes, or users. The broad `except OSError: pass` also suppresses cleanup errors, making unintended deletion behavior and race conditions harder to diagnose. ### Attack Path 1. Another proces ...[truncated 1222 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Store output in a Skill-private cache directory, such as `{baseDir}/.gif_cache`, or in a securely created per-user temporary directory. 2. Use `tempfile.mkdtemp()` or an equivalent secure directory-creation API with restrictive permissions. 3. Give every generated file an unambiguous Skill-specific prefix and purge only matching files. 4. Prefer tracking files created by the current Skill and deleting only those recorded paths. 5. Resolve and validate every cleanup target with `os.path.realpath()` or `pathlib.Path.resolve()`, confirming it remains under the intended private cache root. 6. Avoid deleting arbitrary files based solely on age. 7. Handle cleanup exceptions individually and log actionable errors rather than suppressing all `OSError` exceptions. 8. Consider isolating files by execution or session in separate subdirectories and removing only the completed session directory. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (23)

Tainted flow: 'req' from os.environ.get (line 208, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
url = f"https://api.giphy.com/v1/gifs/search?api_key={api_key}&q={encoded}&limit=3&rating={rating}"

    req = urllib.request.Request(url, headers={"User-Agent": "openclaw-gif-multi/1.0"})
    with urllib.request.urlopen(req) as resp:
        data = json.loads(resp.read().decode())

    if not data.get("data"):
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding
The skill description presents a simple GIF reaction feature, but the documented behavior also performs channel discovery, external API use, local config persistence, cache management, and shell-based media processing. This mismatch can mislead users and reviewers about what data is sent externally and what local/system resources the skill can access.

Credential Access

High
Category
Privilege Escalation
Content
```
   - Or in `~/.openclaw/.env`:
     ```bash
     echo 'GIPHY_API_KEY=your_key' >> ~/.openclaw/.env
     ```

**3. Verify everything is ready**
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
```
   - Or in `~/.openclaw/.env`:
     ```bash
     echo 'GIPHY_API_KEY=your_key' >> ~/.openclaw/.env
     ```

**3. Verify everything is ready**
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
```
   - Or in `~/.openclaw/.env`:
     ```bash
     echo 'GIPHY_API_KEY=your_key' >> ~/.openclaw/.env
     ```

**3. Verify everything is ready**
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
if api_key:
        return api_key
    # Fallback: read OpenClaw .env directly (when skills.entries.*.env is not set)
    env_path = os.path.expanduser("~/.openclaw/.env")
    if os.path.exists(env_path):
        with open(env_path) as f:
            for line in f:
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding
The skill declares significant capabilities including shell, network, file read/write, and environment access, but does not scope or constrain them via explicit permissions. This increases the chance of over-privileged execution and makes review and enforcement difficult, especially because the workflow includes external API calls, config mutation, cache cleanup, and command execution.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The skill advertises auto-detection of channels and GIF search, but does not clearly warn users that their search terms will be transmitted to Giphy and that channel-related context may be used for routing behavior. This creates a privacy/transparency issue because users may not realize conversational content is being sent to a third party.

Session Persistence

Medium
Category
Rogue Agent
Content
## Initial setup

**1. Get a Giphy API Key**
   https://developers.giphy.com → "Create an App" → API (free, 1,000 req/day)

**2. Configure it**
   - Via `openclaw.json` (recommended):
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.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
python3 {baseDir}/scripts/gif_multi.py --mode on_request
```

The user can also say it in conversation: "stop sending GIFs without asking" → switches to `on_request`. "feel free to send GIFs naturally" → switches to `natural`.

## Notes
Confidence
84% confidence
Finding
The documented 'natural' mode authorizes the agent to decide on its own when to send GIFs, which introduces autonomous external actions and communication without a per-action request. In a messaging context, this can produce unintended outputs, spam, or disclosure of conversational cues to a third-party GIF provider.

Vague Triggers

Medium
Confidence
92% confidence
Finding
The skill allows mode changes based on broad conversational phrases like 'feel free to send GIFs naturally,' which can be triggered unintentionally or via prompt manipulation in normal conversation. That can silently switch the skill from explicit-consent behavior to autonomous behavior, causing unrequested external searches and message sends.

Description-Behavior Mismatch

Medium
Confidence
97% confidence
Finding
The manifest and module documentation describe a sending capability across messaging platforms, implying message delivery or platform API interaction. In the implementation, the code discovers channel names, queries Giphy, downloads a GIF, converts it with ffmpeg, and prints JSON output, but contains no platform-send logic or calls to messaging APIs.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The inline documentation actively represents the action as sending content, which contradicts the implementation. The main flow ends by printing a JSON object with the converted file path, and no code performs a send operation to Telegram, WhatsApp, Discord, Signal, or other channels.

Session Persistence

Medium
Category
Rogue Agent
Content
SETUP_HELP = """
━━━ Giphy API Key ━━━

1. Go to https://developers.giphy.com → "Create an App"
2. Select "API" (free, 1,000 requests/day)
3. Copy your API Key
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.

Missing User Warnings

Medium
Confidence
87% confidence
Finding
This code persists configuration to disk and elsewhere in the file also deletes cached/source media files, but there is no user-facing disclosure at the point of execution beyond internal docstrings/comments. For a code file, file writes and deletions should have some visible warning, confirmation, or clear user disclosure unless already clearly covered; here the script silently modifies files under config and .gif_cache locations.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
print(SETUP_HELP)

    # openclaw CLI
    r = subprocess.run(["openclaw", "--version"], capture_output=True, text=True, timeout=10)
    if r.returncode == 0:
        print(f"✅ OpenClaw: {r.stdout.strip()}")
    else:
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
# bins requeridos
    for bin_name in ("python3", "ffmpeg", "curl"):
        r = subprocess.run(["which", bin_name], capture_output=True, text=True, timeout=5)
        if r.returncode == 0:
            print(f"✅ {bin_name}")
        else:
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 discover_channels():
    """Run `openclaw plugins list --json` and extract active channels."""
    result = subprocess.run(
        ["openclaw", "plugins", "list", "--json"],
        capture_output=True, text=True, timeout=15
    )
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The script sends the user's search query and the GIPHY API key in an outbound HTTP request to api.giphy.com, but the code provides no runtime disclosure that user input is being transmitted to a third-party service. Under the code-file criteria, network/HTTP calls that transmit user or system data should have some visible warning, logging, or documented disclosure unless the transmission is clearly disclosed elsewhere.

External Transmission

Medium
Category
Data Exfiltration
Content
return {"error": "GIPHY_API_KEY not found", "help": SETUP_HELP}

    encoded = urllib.parse.quote(query)
    url = f"https://api.giphy.com/v1/gifs/search?api_key={api_key}&q={encoded}&limit=3&rating={rating}"

    req = urllib.request.Request(url, headers={"User-Agent": "openclaw-gif-multi/1.0"})
    with urllib.request.urlopen(req) as resp:
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
"-fs", str(profile["max_bytes"]),
            out_path
        ]
        subprocess.run(cmd, capture_output=True, text=True)
        _cleanup_source(gif_path)
        _purge_old(out_dir, max_age=600)
        return {"channel": channel, "format": "gif", "path": out_path}
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
"-fs", str(profile["max_bytes"]),
            out_path
        ]
        subprocess.run(cmd, capture_output=True, text=True)
        _cleanup_source(gif_path)
        _purge_old(out_dir, max_age=600)
        return {"channel": channel, "format": "gif", "path": out_path}
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Natural-Language Policy Violations

Low
Confidence
82% confidence
Finding
The natural-language comment 'bins requeridos' is in Spanish while the rest of the skill is in English, introducing a language inconsistency without opt-in or documented locale justification. The policy requires avoiding forced language/locale choices unless users are given a choice or the constraint is justified.

Static analysis

No suspicious patterns detected.