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

Content
View full analysis
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
View remediation

T08 · Insecure Dependencies

Warning
Location
scripts/generate_avatar.py:66
Finding

Fallback Mother Image Is Downloaded and Persisted Without Integrity Validation

Content
View full analysis
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
View remediation
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
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/generate_avatar.py (reported line 140)May include surrounding context.

python
headers={**headers, "Content-Type": "application/json"},
        method=method,
    )
    with urllib.request.urlopen(req, timeout=timeout) as r:
        return json.loads(r.read().decode())

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

Critical
Category
Data Flow
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.

Content

Scanner excerpt · scripts/generate_avatar.py (reported line 146)May include surrounding context.

python
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()

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

Critical
Category
Data Flow
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.

Content

Scanner excerpt · scripts/generate_avatar.py (reported line 208)May include surrounding context.

python
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 []

Direct Prompt Extraction

High
Category
System Prompt Leakage
Confidence
85% confidence
Finding

Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Content

Scanner excerpt · references/image-edit-playbook.md (reported line 186)May include surrounding context.

md
- 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

Undeclared Tool Scope

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.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/generate_avatar.py (reported line 15)May include surrounding context.

python
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",

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/generate_avatar.py (reported line 17)May include surrounding context.

python
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",

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/generate_avatar.py (reported line 18)May include surrounding context.

python
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",

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/generate_avatar.py (reported line 19)May include surrounding context.

python
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",

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/generate_avatar.py (reported line 23)May include surrounding context.

python
}
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")

Missing User Warnings

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/generate_avatar.py (reported line 151)May include surrounding context.

python
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}",

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

Medium
Category
Data Flow
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.

Content

Scanner excerpt · scripts/generate_avatar.py (reported line 151)May include surrounding context.

python
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}",

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/generate_avatar.py (reported line 227)May include surrounding context.

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",

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

Medium
Category
Data Flow
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.

Content

Scanner excerpt · scripts/generate_avatar.py (reported line 227)May include surrounding context.

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",

Static analysis

No suspicious patterns detected.