Back to skill

Security audit

Clawy

Security checks for vulnerabilities and agentic risk

Overview

The skill has a legitimate image-generation purpose, but its helper can send API keys and user images to environment-controlled endpoints and downloads a missing default image without integrity checks.

Review before installing. Use only trusted provider endpoints, avoid setting custom BASE_URL environment variables unless you control the destination, use revocable low-scope API keys with spending limits, and consider adding the missing default image locally or requiring checksum validation for the fallback download.

Vulnerability Patterns
  • 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
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (2)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/generate_avatar.py:25
Finding
Unrestricted Custom Provider Endpoints Can Receive API Credentials and User Images<![CDATA[ ## Vulnerability Details **File Location**: `scripts/generate_avatar.py`, lines 25–29, 229–238, 251–265, and 292–322 **Vulnerability Type**: Unvalidated, environment-controlled network destinations for sensitive requests **Risk Level**: High ### Vulnerable Code ```python OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1") ARK_BASE_URL = os.environ.get("ARK_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3") NANO_BASE_URL = os.environ.get("NANO_BASE_URL", "https://generativelanguage.googleapis.com/v1beta") NANO_MODEL = os.environ.get("NANO_MODEL", "gemini-3.1-flash-image-preview") ARK_MODEL = os.environ.get("ARK_MODEL", "doubao-seedream-5-0-260128") ``` ```python def openai_direct_generate(mother_path: Path, prompt: str) -> bytes: api_key = os.environ["OPENAI_API_KEY"] url = OPENAI_BASE_URL.rstrip("/") + "/images/edits" out = subprocess.check_output([ "curl", "-sS", "-X", "POST", url, "-H", f"Authorization: Bearer {api_key}", "-F", "model=gpt-image-1", "-F", f"prompt={prompt}", "-F", f"image=@{mother_path}", "-F", "size=1024x1024", "-F", "quality=high", "-F", "response_format=b64_json", ], text=True) ``` ```python def nano_direct_generate(mother_path: Path, prompt: str) -> bytes: api_key = os.environ.get("GEMINI_API_KEY") or os.environ.get("NANO_API_KEY") if not api_key: raise RuntimeError("Missing GEMINI_API_KEY or NANO_API_KEY") url = NANO_BASE_URL.rstrip("/") + f"/models/{NANO_MODEL}:generateContent?key={api_key}" payload = { "contents": [{ "parts": [ {"text": prompt}, { "inline_data": { "mime_type": guess_mime(mother_path), "data": load_image_b64(mother_path), } }, ] }], "generationConfig": { "responseModalities": ["I ...[truncated 3976 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Allow only documented provider hosts by default: - `api.openai.com` - `generativelanguage.googleapis.com` - The documented Ark provider hostname 2. Parse custom endpoints with a URL parser and reject: - Non-HTTPS schemes - Embedded credentials - Unexpected ports - Loopback, link-local, and private-network destinations unless explicitly authorized 3. Require an explicit command-line option and clear user confirmation before using a custom provider endpoint. 4. Keep custom endpoint support disabled in normal operation or maintain a configurable hostname allowlist. 5. Avoid putting API credentials in query strings. Use a provider-supported authorization header where possible. 6. Avoid placing bearer tokens in `curl` command-line arguments. Prefer an in-process HTTP client or a protected header/configuration input that is not exposed in the process argument list. 7. Document exactly which destination receives each image and credential before generation begins. 8. Use narrowly scoped, revocable API keys with provider-side spending limits and monitor them for unexpected use. ]]>

T08 · Insecure Dependencies

Warning
Location
scripts/generate_avatar.py:66
Finding
Fallback Mother Image Is Downloaded and Persisted Without Integrity Validation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/generate_avatar.py`, lines 66–74 **Vulnerability Type**: Unverified remote asset retrieval **Risk Level**: Medium ### Vulnerable Code ```python def default_mother_image() -> Path: skill_dir = Path(__file__).resolve().parent.parent asset_path = skill_dir / "assets" / "default-mother-image.png" if asset_path.exists(): return asset_path asset_path.parent.mkdir(parents=True, exist_ok=True) fallback_url = "https://www.8uddy.land/images/clawy.png" asset_path.write_bytes(http_get_bytes(fallback_url)) return asset_path ``` ### Technical Analysis The project documentation describes `assets/default-mother-image.png` as a bundled file, but the audited directory does not contain that asset. Consequently, invoking generation without a custom mother image causes the script to download content from `https://www.8uddy.land/images/clawy.png`. HTTPS protects the connection in normal circumstances, but the script does not verify a pinned cryptographic digest, decoded image format, image dimensions, or maximum response size. It then persistently writes the response into the project directory and treats it as a trusted reference image. This does not constitute remote code retrieval and execution: the downloaded object is used as image data rather than executed. Nevertheless, compromise of the remote host, its delivery infrastructure, or the expected asset could silently change the skill's effective input after review. ### Attack Path 1. The local `assets/default-mother-image.png` file is absent, as it is in the audited artifact. 2. The user invokes avatar generation without supplying a custom mother image. 3. The script automatically requests the fallback URL. 4. The remote host or its delivery path returns substituted, malformed, or excessively large content. 5. The script writes that content to `assets/default-mother-image.png` without validation. 6. The stored content is ...[truncated 1008 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Include the declared `assets/default-mother-image.png` directly in the skill package. 2. If fallback downloading remains necessary, pin the approved file's SHA-256 digest and reject any mismatch before writing or using it. 3. Enforce a conservative maximum response size while streaming the download. 4. Decode and validate the asset as an expected image format rather than relying on the URL extension. 5. Verify expected dimensions, color mode, and other relevant image properties. 6. Download to a temporary file, validate it completely, and then atomically move it into place. 7. Fail closed if validation fails, leaving any existing trusted asset untouched. 8. Consider requiring explicit user consent before downloading and persisting a missing asset. 9. Document the trusted asset origin and update process so digest changes receive security review. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • System Prompt LeakageDirect Leakage, Indirect Extraction, Tool-Based Exfiltration
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (16)

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

Critical
Category
Data Flow
Content
headers={**headers, "Content-Type": "application/json"},
        method=method,
    )
    with urllib.request.urlopen(req, timeout=timeout) as r:
        return json.loads(r.read().decode())
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

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

Critical
Category
Data Flow
Content
def http_get_bytes(url: str, headers: Optional[dict] = None, timeout: int = 180) -> bytes:
    req = urllib.request.Request(url, headers=headers or {})
    with urllib.request.urlopen(req, timeout=timeout) as r:
        return r.read()
Confidence
96% confidence
Finding
http_get_bytes fetches arbitrary URLs and is used on attacker-influenceable values such as the fallback default image URL and provider-returned result URLs. This creates an SSRF-style primitive and untrusted remote content fetch path that can be abused to make outbound requests, download unexpected content, or persist malicious payloads to disk under the guise of an image.

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

Critical
Category
Data Flow
Content
img_url = None
    for _ in range(120):
        req = urllib.request.Request(result_url, headers={"Authorization": f"Bearer {api_key}"})
        with urllib.request.urlopen(req, timeout=120) as r:
            body = json.loads(r.read().decode())
        data = body.get("data", body)
        outputs = data.get("outputs") or []
Confidence
90% confidence
Finding
The polling loop follows a result URL obtained from a remote API response and sends an Authorization header with the API key. If the upstream response is compromised or malicious, the code may transmit the bearer token to an attacker-controlled URL, turning the polling mechanism into a credential exfiltration channel.

Direct Prompt Extraction

High
Category
System Prompt Leakage
Content
- not hands replacing claws
- not extra legs or humanoid limb structure

### Event Image Output Rule
When the user wants a Clawy travel post /朋友圈 / Instagram-style event update:
- generate the image first
- pair it with a short in-character caption
Confidence
85% confidence
Finding
Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill advertises and relies on capabilities including environment access, file read/write, network access, and shell execution, but it does not declare an explicit tool scope such as permissions or allowed-tools. That makes the effective privilege boundary unclear to the host and reviewers, increasing the risk that an agent can invoke broader capabilities than intended, especially since the skill includes a helper script, external credential use, network downloads, and local file writes.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The playbook states that captions should 'usually be within 30 Chinese characters,' which imposes a specific language choice as a default behavior. This is a natural-language policy issue because it does not offer the user a language choice or clearly justify the locale restriction.

External Transmission

Medium
Category
Data Exfiltration
Content
from urllib.error import HTTPError, URLError
from typing import Dict, List, Optional, Tuple

WAVESPEED_UPLOAD = "https://api.wavespeed.ai/api/v3/media/upload/binary"
WAVESPEED_APIS = {
    "nano": "https://api.wavespeed.ai/api/v3/google/nano-banana-2/edit",
    "openai": "https://api.wavespeed.ai/api/v3/openai/gpt-image-1.5/edit",
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
from urllib.error import HTTPError, URLError
from typing import Dict, List, Optional, Tuple

WAVESPEED_UPLOAD = "https://api.wavespeed.ai/api/v3/media/upload/binary"
WAVESPEED_APIS = {
    "nano": "https://api.wavespeed.ai/api/v3/google/nano-banana-2/edit",
    "openai": "https://api.wavespeed.ai/api/v3/openai/gpt-image-1.5/edit",
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
from urllib.error import HTTPError, URLError
from typing import Dict, List, Optional, Tuple

WAVESPEED_UPLOAD = "https://api.wavespeed.ai/api/v3/media/upload/binary"
WAVESPEED_APIS = {
    "nano": "https://api.wavespeed.ai/api/v3/google/nano-banana-2/edit",
    "openai": "https://api.wavespeed.ai/api/v3/openai/gpt-image-1.5/edit",
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
from urllib.error import HTTPError, URLError
from typing import Dict, List, Optional, Tuple

WAVESPEED_UPLOAD = "https://api.wavespeed.ai/api/v3/media/upload/binary"
WAVESPEED_APIS = {
    "nano": "https://api.wavespeed.ai/api/v3/google/nano-banana-2/edit",
    "openai": "https://api.wavespeed.ai/api/v3/openai/gpt-image-1.5/edit",
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
}
WAVESPEED_PREFERRED_ORDER = ["nano", "openai", "fast"]

OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1")
ARK_BASE_URL = os.environ.get("ARK_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3")
NANO_BASE_URL = os.environ.get("NANO_BASE_URL", "https://generativelanguage.googleapis.com/v1beta")
NANO_MODEL = os.environ.get("NANO_MODEL", "gemini-3.1-flash-image-preview")
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
When the default asset is missing, the script silently downloads an image from an external domain and writes it to disk. In a skill that claims only selected images are sent externally when generation is invoked, this hidden network fetch broadens data flow unexpectedly and introduces supply-chain risk from an unpinned remote asset.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def wavespeed_upload_image(path: Path, api_key: str) -> str:
    out = subprocess.check_output([
        "curl", "-sS", "-X", "POST", WAVESPEED_UPLOAD,
        "-H", f"Authorization: Bearer {api_key}",
        "-F", f"file=@{path}",
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Tainted flow: 'api_key' from os.environ (line 293, credential/environment) → subprocess.check_output (code execution)

Medium
Category
Data Flow
Content
def wavespeed_upload_image(path: Path, api_key: str) -> str:
    out = subprocess.check_output([
        "curl", "-sS", "-X", "POST", WAVESPEED_UPLOAD,
        "-H", f"Authorization: Bearer {api_key}",
        "-F", f"file=@{path}",
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def openai_direct_generate(mother_path: Path, prompt: str) -> bytes:
    api_key = os.environ["OPENAI_API_KEY"]
    url = OPENAI_BASE_URL.rstrip("/") + "/images/edits"
    out = subprocess.check_output([
        "curl", "-sS", "-X", "POST", url,
        "-H", f"Authorization: Bearer {api_key}",
        "-F", "model=gpt-image-1",
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Tainted flow: 'url' from os.environ.get (line 294, credential/environment) → subprocess.check_output (code execution)

Medium
Category
Data Flow
Content
def openai_direct_generate(mother_path: Path, prompt: str) -> bytes:
    api_key = os.environ["OPENAI_API_KEY"]
    url = OPENAI_BASE_URL.rstrip("/") + "/images/edits"
    out = subprocess.check_output([
        "curl", "-sS", "-X", "POST", url,
        "-H", f"Authorization: Bearer {api_key}",
        "-F", "model=gpt-image-1",
Confidence
81% confidence
Finding
OPENAI_BASE_URL is taken from the environment and used as the destination for a credentialed curl request. In an agent/runtime setting where environment configuration may be influenced by deployment or another component, this can redirect the mother's image, prompt, and Authorization bearer token to an attacker-controlled server, causing credential and data exfiltration.

Static analysis

No suspicious patterns detected.