Back to skill

Security audit

Runpod Media

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its media-generation purpose, but it needs Review because it reads unrelated Telegram credentials and can upload arbitrary local files to external services.

Install only if you are comfortable with the skill sending prompts and media to RunPod, temporarily uploading local inputs to Cloudflare R2, and potentially delivering outputs through Telegram. Review or remove the Telegram curl fallback before use, restrict which local files may be uploaded, and avoid arbitrary endpoint IDs unless you trust the endpoint and understand where the data will go.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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
Findings (4)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
SKILL.md:189
Finding
Direct Access to Telegram Bot Credentials Bypasses the Protected Messaging Abstraction<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:189-220` **Vulnerability Type**: Least-privilege violation and direct credential access **Risk Level**: High ### Vulnerable Code ```bash TOKEN=$(cat ~/.openclaw/secrets.json | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('telegram',{}).get('botToken',''))") # Send photo curl -s \ -F "chat_id=CHAT_ID" \ -F "photo=@$HOME/.openclaw/workspace/runpod-media/OUTPUT_FILE.jpg" \ -F "caption=YOUR CAPTION" \ "https://api.telegram.org/bot${TOKEN}/sendPhoto" # Send video (.mp4) curl -s \ -F "chat_id=CHAT_ID" \ -F "video=@$HOME/.openclaw/workspace/runpod-media/OUTPUT_FILE.mp4" \ -F "caption=YOUR CAPTION" \ "https://api.telegram.org/bot${TOKEN}/sendVideo" ``` The instructions further direct the agent to use this mechanism when the normal messaging abstraction fails: ```markdown Try it first — if it works, great. If it returns a SecretRef error, fall back to the curl method above. ``` ### Technical Analysis The Skill's declared media-generation functionality requires RunPod and Cloudflare R2 credentials. It does not inherently require direct access to the Telegram bot token. These instructions explicitly tell the agent to read an unrelated high-value credential from the global OpenClaw secrets file and use it through a shell command. This bypasses the platform's `message` tool and its SecretRef isolation specifically when sandboxed credential resolution prevents delivery. The bot token is interpolated into the curl URL. Consequently, it can become visible through process-command inspection, shell tracing, diagnostic tooling, or error collection. Direct API access also removes policy and validation controls that the platform messaging abstraction may otherwise enforce. ### Attack Path 1. A user requests image or video generation. 2. The Skill creates the requested media. 3. Delivery through the normal `message` tool fails or is represented as having failed with a Sec ...[truncated 845 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove the direct `secrets.json` access and curl-based Telegram fallback from `SKILL.md`. 2. Deliver media exclusively through the platform-provided `message` tool. 3. If sandbox-compatible delivery is required, implement a trusted platform broker that accepts a media reference without exposing the Telegram token to the Skill. 4. Restrict the Skill manifest to the RunPod and R2 secrets necessary for generation and temporary uploads. 5. Ensure messaging credentials never appear in shell arguments, logs, diagnostics, environment variables inherited by child processes, or generated instructions. 6. Treat a SecretRef failure as a delivery error rather than authorization to bypass the isolation mechanism. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/_utils.py:59
Finding
Arbitrary Readable Local Files Can Be Uploaded to Cloudflare R2<![CDATA[ ## Vulnerability Details **File Location**: `scripts/_utils.py:59-96` and `scripts/_utils.py:149-154` **Vulnerability Type**: Unrestricted local file upload and information disclosure **Risk Level**: High ### Vulnerable Code ```python def upload_to_r2(path: str, expiry: int = 60) -> str: """Upload a local file to Cloudflare R2 and return a presigned GET URL (default 1 min).""" import boto3 from botocore.config import Config secrets_path = pathlib.Path.home() / ".openclaw" / "secrets.json" data = json.loads(secrets_path.read_text()) r2 = data.get("cloudflare", {}).get("r2", {}) access_key = r2.get("accessKeyId") secret_key = r2.get("secretAccessKey") endpoint = r2.get("endpoint") bucket = r2.get("bucket", "openclaw") if not all([access_key, secret_key, endpoint]): raise SystemExit("R2 credentials not found in secrets.json under cloudflare.r2") client = boto3.client( "s3", endpoint_url=endpoint, aws_access_key_id=access_key, aws_secret_access_key=secret_key, region_name="auto", config=Config(signature_version="s3v4"), ) file_path = pathlib.Path(path) key = f"uploads/{int(time.time())}_{file_path.name}" content_type = mimetypes.guess_type(str(file_path))[0] or "application/octet-stream" with open(file_path, "rb") as f: client.put_object(Bucket=bucket, Key=key, Body=f, ContentType=content_type) url = client.generate_presigned_url( "get_object", Params={"Bucket": bucket, "Key": key}, ExpiresIn=expiry, ) print(f" uploaded to R2: {url[:80]}…") return url ``` ```python def ensure_url(path_or_url: str, imgbb_key: str | None = None) -> str: """Return a public URL, uploading to R2 (presigned, 1 min) if a local path is given.""" if path_or_url.startswith("http://") or path_or_url.startswith("https://"): return path_or_url return upload_to_r2(path_or_url, expiry=6 ...[truncated 2166 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Resolve each local path with `Path.resolve(strict=True)` and require it to remain under a dedicated, approved media-input directory. 2. Reject symlinks, device files, directories, sockets, and all other non-regular files. 3. Decode and validate actual image or audio content rather than trusting the filename or `mimetypes.guess_type()`. 4. Enforce strict format and file-size allowlists before opening or uploading content. 5. Require explicit user confirmation before uploading local files that were not attached to the current request. 6. Use a dedicated R2 credential restricted to one bucket and an upload-only prefix. It should not have account-wide or unrelated bucket permissions. 7. Delete uploaded objects immediately after the downstream service has retrieved them rather than relying solely on a one-day lifecycle policy. 8. Avoid displaying any portion of presigned URLs in logs. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/_utils.py:158
Finding
Untrusted Endpoint Response URLs Are Downloaded Without Network or Resource Restrictions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/_utils.py:158-170` **Vulnerability Type**: Server-side request forgery and unbounded remote-content download **Risk Level**: High ### Vulnerable Code ```python def get_media_url(output: dict) -> str | None: return ( output.get("video_url") or output.get("image_url") or output.get("result") or output.get("url") or output.get("image") ) def output_dir() -> pathlib.Path: # Save inside OpenClaw workspace so sandboxed agents can read the files d = pathlib.Path.home() / ".openclaw" / "workspace" / "runpod-media" d.mkdir(parents=True, exist_ok=True) return d def save_media(url: str, filename: str) -> pathlib.Path: dest = output_dir() / filename resp = requests.get(url, timeout=120) resp.raise_for_status() dest.write_bytes(resp.content) return dest ``` The generic caller accepts any endpoint ID and passes its output to this function: ```python if output_key and isinstance(output, dict): url = output.get(output_key) if not url: url = get_media_url(output) if url: dest = save_media(url, f"{args.endpoint.replace('-','_')}_{ts}.{ext}") ``` ### Technical Analysis A URL returned by a selected RunPod endpoint is trusted and fetched by the local Skill process. The implementation does not validate: - The URL scheme - The destination hostname - Whether the resolved address is private, loopback, link-local, or otherwise internal - Redirect destinations - The returned content type or media signature - The response `Content-Length` - The total number of bytes downloaded `requests.get()` follows redirects by default. It also buffers the complete response in `resp.content` before writing it, allowing a large response to exhaust process memory and consume disk space. Because `call_endpoint.py` intentionally supports arbitrary RunPod endpoint IDs, the returned media location cannot safely be assumed to belong to a ...[truncated 1238 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Permit only HTTPS media URLs from an explicit allowlist of trusted RunPod or storage domains. 2. Resolve the destination before connecting and reject loopback, private, link-local, multicast, reserved, and metadata-service address ranges for both IPv4 and IPv6. 3. Disable redirects or independently revalidate the scheme, hostname, and resolved address after every redirect. 4. Download using `stream=True` and stop once a strict maximum size is exceeded. 5. Apply connection and read timeouts separately. 6. Validate `Content-Type`, then verify image, video, or audio magic bytes before retaining the file. 7. Download to a safely created temporary file and atomically rename it only after validation succeeds. 8. Prefer retrieving generated media through a trusted RunPod API operation rather than following arbitrary endpoint-provided URLs. ]]>

T08 · Insecure Dependencies

Warning
Location
scripts/run.sh:17
Finding
Runtime Dependencies Are Unpinned and Resolved During Skill Execution<![CDATA[ ## Vulnerability Details **File Location**: `scripts/run.sh:17-22` and `scripts/_utils.py:1-3` **Vulnerability Type**: Unpinned runtime dependency resolution **Risk Level**: Medium ### Vulnerable Code ```bash case "$COMMAND" in generate_image|edit_image|image_to_video|text_to_video|call_endpoint|discover_endpoints) exec uv run "$SCRIPT_DIR/${COMMAND}.py" "$@" ;; list_endpoints) exec uv run "$SCRIPT_DIR/call_endpoint.py" --list ;; ``` The inline dependency declaration does not specify versions or hashes: ```python # /// script # dependencies = ["requests", "boto3"] # /// ``` Other executable scripts similarly declare unpinned `requests` dependencies. ### Technical Analysis The launcher uses `uv run` directly against scripts containing PEP 723-style dependency metadata. In an environment where packages are not already available in the cache, `uv` resolves and installs packages as part of command execution. Dependencies such as `requests` and `boto3` are identified only by package name. There is no exact version pin, lockfile, integrity hash, or repository restriction represented in the project. The effective code executed by the Skill can therefore change over time without any modification to the reviewed package. The package names observed are established packages rather than obvious typosquatting names. The primary issue is nevertheless non-reproducible runtime resolution and exposure to future package, index, or dependency-chain compromise. ### Attack Path 1. The Skill runs in a new environment or one without the required packages cached. 2. `run.sh` invokes `uv run` for the selected Python program. 3. `uv` resolves the unpinned package names and their transitive dependencies from its configured package source. 4. A compromised, malicious, or unexpectedly changed release is selected. 5. Package code executes in the Skill's process context. 6. That code inherits access to local files, network connectivity, and the crede ...[truncated 519 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin every direct dependency to an audited exact version. 2. Generate and commit a lockfile that also fixes all transitive dependency versions. 3. Require package hashes or equivalent integrity verification. 4. Restrict installation to an explicitly configured trusted package index. 5. Install dependencies during a controlled build or setup phase rather than resolving them when handling a user request. 6. Use a prebuilt, signed environment or immutable container image where practical. 7. Add automated dependency vulnerability and provenance scanning to the release process. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (48)

Tainted flow: 'api_key' from os.getenv (line 34, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Content
def runsync(endpoint_id: str, payload: dict, api_key: str, timeout: int = 120) -> dict:
    url = f"https://api.runpod.ai/v2/{endpoint_id}/runsync"
    resp = requests.post(url, json={"input": payload}, headers=_headers(api_key), timeout=timeout)
    resp.raise_for_status()
    data = resp.json()
    if data.get("status") == "FAILED":
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'api_key' from os.getenv (line 34, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Content
def runsync(endpoint_id: str, payload: dict, api_key: str, timeout: int = 120) -> dict:
    url = f"https://api.runpod.ai/v2/{endpoint_id}/runsync"
    resp = requests.post(url, json={"input": payload}, headers=_headers(api_key), timeout=timeout)
    resp.raise_for_status()
    data = resp.json()
    if data.get("status") == "FAILED":
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'api_key' from os.getenv (line 34, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
url = f"https://api.runpod.ai/v2/{endpoint_id}/status/{job_id}"
    deadline = time.time() + max_wait
    while time.time() < deadline:
        resp = requests.get(url, headers=_headers(api_key), timeout=30)
        resp.raise_for_status()
        data = resp.json()
        status = data.get("status")
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
97% confidence
Finding
The skill also supports endpoint discovery, probing, health verification, and local registry modification, none of which are disclosed in the core description. These capabilities materially expand the attack surface by enabling dynamic expansion of what the skill can contact and persist for future use.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The skill also supports endpoint discovery, probing, health verification, and local registry modification, none of which are disclosed in the core description. These capabilities materially expand the attack surface by enabling dynamic expansion of what the skill can contact and persist for future use.

Credential Access

High
Category
Privilege Escalation
Content
## API Keys

One key required — add to `~/.openclaw/secrets.json`:

| Key path | Purpose | Get it from |
|----------|---------|-------------|
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
## API Keys

One key required — add to `~/.openclaw/secrets.json`:

| Key path | Purpose | Get it from |
|----------|---------|-------------|
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
## API Keys

One key required — add to `~/.openclaw/secrets.json`:

| Key path | Purpose | Get it from |
|----------|---------|-------------|
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
## API Keys

One key required — add to `~/.openclaw/secrets.json`:

| Key path | Purpose | Get it from |
|----------|---------|-------------|
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
## API Keys

One key required — add to `~/.openclaw/secrets.json`:

| Key path | Purpose | Get it from |
|----------|---------|-------------|
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
## API Keys

One key required — add to `~/.openclaw/secrets.json`:

| Key path | Purpose | Get it from |
|----------|---------|-------------|
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
## API Keys

One key required — add to `~/.openclaw/secrets.json`:

| Key path | Purpose | Get it from |
|----------|---------|-------------|
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
## API Keys

One key required — add to `~/.openclaw/secrets.json`:

| Key path | Purpose | Get it from |
|----------|---------|-------------|
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
## API Keys

One key required — add to `~/.openclaw/secrets.json`:

| Key path | Purpose | Get it from |
|----------|---------|-------------|
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
## API Keys

One key required — add to `~/.openclaw/secrets.json`:

| Key path | Purpose | Get it from |
|----------|---------|-------------|
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Context-Inappropriate Capability

High
Confidence
97% confidence
Finding
The skill directs the agent to read Telegram bot credentials from secrets and exfiltrate generated files via direct API calls unrelated to the core RunPod function. This bypasses normal tool-mediated delivery controls, introduces another third-party recipient, and expands the skill into credential use plus outbound messaging.

External Script Fetching

High
Category
Supply Chain
Content
### The Problem
The `message` tool with a local `media` path may fail in sandboxed agent modes due to SecretRef resolution not being available for media sends. This is a known OpenClaw limitation.

### The Solution: Use curl + Telegram Bot API directly

Read the bot token from secrets and send via curl — this always works regardless of sandbox mode:
Confidence
90% confidence
Finding
Remote code is downloaded and executed. This bypasses code review and could introduce malicious code.

Credential Access

High
Category
Privilege Escalation
Content
Read the bot token from secrets and send via curl — this always works regardless of sandbox mode:

```bash
TOKEN=$(cat ~/.openclaw/secrets.json | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('telegram',{}).get('botToken',''))")

# Send photo
curl -s \
Confidence
98% confidence
Finding
This instruction explicitly tells the agent to read a Telegram bot token from `~/.openclaw/secrets.json` and use it in direct API requests. Accessing unrelated messaging credentials from within a media-generation skill breaks separation of duties and enables unauthorized outbound communications if abused.

Credential Access

High
Category
Privilege Escalation
Content
def _r2_config() -> dict:
    """Load R2 config from secrets.json."""
    cfg = _from_secrets_json("/cloudflare/r2/accessKeyId")
    if not cfg:
        # Return full dict
Confidence
88% confidence
Finding
This function is specifically designed to load Cloudflare R2 configuration from the shared secrets store, which is outside the skill's stated RunPod-only need. In context, accessing unrelated credentials increases risk because it grants the skill additional capabilities to move data externally.

Credential Access

High
Category
Privilege Escalation
Content
cfg = _from_secrets_json("/cloudflare/r2/accessKeyId")
    if not cfg:
        # Return full dict
        secrets_path = pathlib.Path.home() / ".openclaw" / "secrets.json"
        data = json.loads(secrets_path.read_text())
        return data.get("cloudflare", {}).get("r2", {})
    return {}
Confidence
89% confidence
Finding
Reading the broader secrets.json structure to return Cloudflare R2 configuration expands access beyond the minimum needed for RunPod operations. Even if not immediately exfiltrated on this line, it unnecessarily exposes additional credentials to the skill runtime.

Credential Access

High
Category
Privilege Escalation
Content
import boto3
    from botocore.config import Config

    secrets_path = pathlib.Path.home() / ".openclaw" / "secrets.json"
    data = json.loads(secrets_path.read_text())
    r2 = data.get("cloudflare", {}).get("r2", {})
Confidence
94% confidence
Finding
This code reads the secrets file to retrieve Cloudflare R2 credentials for actual use in external storage operations. In context, that is a meaningful security concern because the skill now has access to a second set of sensitive credentials unrelated to the user-visible RunPod function.

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill declares no explicit tool scope despite clearly requiring filesystem, environment, and network access. In a skill system, missing scope boundaries increases the chance the agent can use broader capabilities than users or reviewers expect, weakening least-privilege controls.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The skill sends local images to Cloudflare R2 and then RunPod, but the description does not provide a clear user-facing warning that local files are uploaded to third-party services. Users may unknowingly expose sensitive media to external storage and AI providers.

Context-Inappropriate Capability

Medium
Confidence
92% confidence
Finding
The skill encourages discovery and addition of new RunPod endpoints, which lets the skill's operational scope grow over time without equivalent security review. That creates a path for unsafe or privacy-invasive endpoints to be introduced into future sessions.

Context-Inappropriate Capability

Medium
Confidence
96% confidence
Finding
Allowing `call_endpoint` against arbitrary endpoint IDs broadens the skill beyond its declared media role and can route prompts, files, or audio to services with different trust assumptions. The danger is amplified because users may believe they are using a narrowly scoped media tool when in fact they are invoking any compatible public endpoint.

Static analysis

No suspicious patterns detected.