Back to skill

Security audit

OpenRouter Image Generation

Security checks for vulnerabilities and agentic risk

Overview

This image-generation skill is mostly coherent, but its reference-image option can upload any readable local file to OpenRouter if given that path.

Review before installing. Use it only with non-sensitive prompts and reference images, keep outputs in an intended workspace path, and do not pass arbitrary local paths as --ref. Prefer setting OPENROUTER_API_KEY through the environment, and consider adding image validation, file-size limits, path restrictions, and a clear external-upload warning before broad use.

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

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/generate.py:24
Finding
Arbitrary Local File Disclosure Through Unvalidated Reference Image Path<![CDATA[ ## Vulnerability Details **File Location**: `scripts/generate.py`, lines 24-34 and 51-69 **Vulnerability Type**: Unrestricted local file read followed by network transmission **Risk Level**: Medium ### Vulnerable Code ```python def encode_image(path: str) -> dict: """Read an image file and return an OpenAI-style image_url content part.""" mime, _ = mimetypes.guess_type(path) if mime is None: mime = "image/png" with open(path, "rb") as f: b64 = base64.b64encode(f.read()).decode() return { "type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}, } def generate( prompt: str, output: str, ref_image: str | None = None, model: str = "google/gemini-3.1-flash-image-preview", api_key: str | None = None, base_url: str = "https://openrouter.ai/api/v1", ) -> str: """Generate an image and save to *output*. Returns the output path.""" api_key = api_key or os.environ.get("OPENROUTER_API_KEY") if not api_key: sys.exit("Error: OPENROUTER_API_KEY not set and --api-key not provided.") # Build message content content: list[dict] = [] if ref_image: content.append(encode_image(ref_image)) content.append({"type": "text", "text": prompt}) payload = { "model": model, "modalities": ["text", "image"], "messages": [{"role": "user", "content": content}], "max_tokens": 4096, } data = json.dumps(payload).encode() req = urllib.request.Request( f"{base_url}/chat/completions", data=data, headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", }, method="POST", ) ``` ### Technical Analysis The `--ref` argument is documented as a reference image, but `encode_image()` accepts any readable path. The implementation relies on the filename extension to infer a MIME type and defaults unknown files to `im ...[truncated 2203 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. **Validate actual image content**: Parse the file with a trusted image decoder and reject data that cannot be decoded as a supported image format. Do not rely solely on the filename extension or `mimetypes.guess_type()`. 2. **Restrict permitted formats**: Allow only explicitly supported formats such as PNG, JPEG, and WebP, and derive the MIME type from validated content. 3. **Enforce path boundaries**: Resolve the path with `Path.resolve()` and require it to be under an approved workspace or media directory. Reject traversal, symlink escapes, and paths outside authorized roots. 4. **Set a file-size limit**: Check the file size before reading it and reject oversized references. This also limits memory exhaustion and excessive request sizes. 5. **Require informed confirmation**: If references outside an approved workspace must be supported, display the resolved path and destination service and require explicit confirmation before upload. 6. **Apply least privilege**: Run the Skill under an account or sandbox that cannot read unrelated credential stores, private keys, or system files. 7. **Document external disclosure clearly**: State that the selected reference image is transmitted to OpenRouter and may be processed under the provider's retention and privacy policies. 8. **Add security tests**: Verify that non-image files, oversized files, symlink escapes, traversal attempts, and paths outside approved roots are rejected before any network request occurs. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (6)

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

Critical
Category
Data Flow
Content
)

    try:
        with urllib.request.urlopen(req, timeout=120) as resp:
            result = json.loads(resp.read())
    except urllib.error.HTTPError as e:
        body = e.read().decode(errors="replace")
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Missing User Warnings

High
Confidence
98% confidence
Finding
The documentation explains that prompts and optional reference images are sent to OpenRouter/Gemini but does not clearly warn users that their content is transmitted to an external third-party API. This creates a meaningful privacy and data-handling risk, especially if users provide sensitive prompts, personal images, or proprietary material.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill invokes code that can access environment variables, write files, and make network requests, but the manifest does not declare any tool scope or permissions boundaries. This weakens reviewability and least-privilege controls, making it easier for a broadly-triggered skill to exfiltrate secrets or write unexpected files without clear operator awareness.

Vague Triggers

Medium
Confidence
95% confidence
Finding
The trigger description uses very broad phrases like generate, create, draw, or design images, illustrations, covers, and avatars, which can cause the skill to activate for many ordinary user requests. In combination with external network calls and file output, over-broad routing increases the chance that user content is sent to a third party unexpectedly.

Natural-Language Policy Violations

Medium
Confidence
83% confidence
Finding
The text states that English text works reasonably well and advises users to avoid Chinese/Japanese because the model usually garbles those characters. This imposes a language-specific preference in the skill guidance without offering a user choice or framing it as a documented, justified technical limitation with neutral alternatives.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The script transmits user prompts and optional reference images to a third-party service, which can expose sensitive text or image content if users assume processing is local. In this skill context, reference images may contain personal, proprietary, or confidential material, so the lack of an explicit disclosure or consent step increases privacy and data handling risk.

Static analysis

No suspicious patterns detected.