Back to skill

Security audit

VVMAI Image Generator

Security checks for vulnerabilities and agentic risk

Overview

This image skill mostly matches its image-generation purpose, but it has underdocumented edit/upload behavior and weak safeguards that could expose API keys, user content, or local files.

Review before installing. Use this only if you trust VVMAI and the configured endpoint, keep VVMAI_BASE_URL pinned to the intended HTTPS service, avoid sensitive prompts or images, avoid remote --input URLs, and do not use path components in --filename unless the script is fixed to validate destinations and download limits.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/gen.py:60
Finding
Configurable API Endpoint Can Expose the API Key and User Content<![CDATA[ ## Vulnerability Details **File Location**: `scripts/gen.py`, lines 60-75 and 196-197 **Vulnerability Type**: Unrestricted transmission of credentials and user content to a configurable endpoint **Risk Level**: High ### Vulnerable Code ```python def request_generate( base_url: str, api_key: str, prompt: str, model: str, response_format: str, size: str = "", aspect_ratio: str = "", count: int = 1, ) -> dict: """POST /images/generations""" url = f"{base_url.rstrip('/')}/images/generations" payload = { "model": model, "prompt": prompt, "n": count, "response_format": response_format, } if size: payload["size"] = size if aspect_ratio: payload["aspect_ratio"] = aspect_ratio body = json.dumps(payload).encode("utf-8") req = urllib.request.Request( url, method="POST", headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, data=body, ) ``` ```python api_key = os.environ.get("VVMAI_API_KEY", "").strip() base_url = os.environ.get("VVMAI_BASE_URL", "https://api.vvmai.com/v1").strip() ``` The same endpoint construction and bearer-token transmission pattern is also used by `request_edit`, where an input image is included in the request. ### Technical Analysis The Skill legitimately needs to transmit an API key and prompt to the declared VVMAI image-generation service. However, `VVMAI_BASE_URL` is accepted without validating its scheme or destination host. Consequently, the bearer credential and request content can be sent to any configured host, including a plaintext HTTP endpoint. This exceeds the minimum network privilege needed for the default functionality. A Skill intended to access VVMAI only needs access to VVMAI's authenticated HTTPS endpoint. Supporting arbitrary endpoints without an explicit trust boundary allows a modified environment or configuration file to redirect sensitive requ ...[truncated 1370 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require HTTPS for all API endpoints and reject `http://` or other schemes. 2. Restrict the default configuration to the documented VVMAI hostname, such as `api.vvmai.com`. 3. If custom OpenAI-compatible endpoints are necessary, require explicit opt-in and display a warning that the API key and content will be sent to the selected host. 4. Do not send a VVMAI credential to a host outside an approved allowlist. Use separate credentials for custom providers. 5. Parse the endpoint with `urllib.parse.urlsplit` and validate the normalized scheme, hostname, port, user-information component, and final redirect destination. 6. Disable cross-host redirects for authenticated requests or strip the `Authorization` header whenever a redirect changes the origin. 7. Document clearly that prompts and edit images leave the local system and identify the receiving service. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/gen.py:31
Finding
Arbitrary Input-Image URL Fetching Enables SSRF and Unbounded Downloads<![CDATA[ ## Vulnerability Details **File Location**: `scripts/gen.py`, lines 31-44 and 217-220 **Vulnerability Type**: Server-side request forgery and uncontrolled resource consumption **Risk Level**: High ### Vulnerable Code ```python def download_to_temp(url: str) -> Path: """Download remote image to a temp file with chunked read.""" import tempfile suffix = ".jpg" if ".jpg" in url.lower() else ".png" tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix) req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) with urllib.request.urlopen(req, timeout=60) as resp: with open(tmp.name, "wb") as f: while True: chunk = resp.read(65536) if not chunk: break f.write(chunk) return Path(tmp.name) ``` ```python if args.input: # Edit mode input_path = args.input.strip() if input_path.startswith("http"): print(f"Downloading input image...", file=sys.stderr) local_input = download_to_temp(input_path) else: local_input = Path(input_path) ``` ### Technical Analysis Edit mode treats any string beginning with `http` as a remotely downloadable image. The implementation does not: - Restrict requests to public HTTPS destinations. - Reject loopback, private, link-local, multicast, or reserved IP ranges. - Block cloud instance-metadata addresses. - Revalidate destinations after DNS resolution or redirects. - Verify that the response is actually an image. - Limit the total number of downloaded bytes. - Remove the temporary file after the edit request completes. The 60-second timeout limits an individual network operation but does not impose a maximum response size. A sufficiently fast endpoint can therefore write a large amount of data to disk within the timeout. Redirect handling by the URL library may also move the request from an apparently public URL to an internal destination unless explicitly ...[truncated 2057 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Accept only `https://` input URLs unless a documented use case requires another scheme. 2. Resolve the destination hostname before connecting and reject loopback, private, link-local, multicast, unspecified, and reserved IP ranges for both IPv4 and IPv6. 3. Revalidate every redirect destination and every resolved address to prevent redirect-based SSRF and DNS rebinding. 4. Consider an allowlist of trusted image-hosting domains rather than unrestricted public URLs. 5. Disable redirects by default or enforce a small redirect count with same-origin restrictions. 6. Require an image MIME type and verify the downloaded file signature rather than trusting the URL suffix or response header alone. 7. Enforce a strict maximum download size using both `Content-Length` where available and a cumulative byte counter while streaming. 8. Apply conservative connection and read timeouts. 9. Avoid loading the entire file into memory; stream a size-limited upload where practical. 10. Track whether the file was downloaded by the script and remove it in a `finally` block after the API operation. 11. Create temporary files with restrictive permissions and close the original `NamedTemporaryFile` handle before reopening it. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/gen.py:145
Finding
Unsanitized Output Filename Allows Writes Outside the Selected Directory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/gen.py`, lines 145-163 and 187 **Vulnerability Type**: Path traversal and arbitrary file overwrite **Risk Level**: Medium ### Vulnerable Code ```python def process_response(data: list, response_format: str, should_save: bool, out_dir: Path, prompt: str, count: int, filename: str) -> None: for idx, item in enumerate(data): image_url = item.get("url") image_b64 = item.get("b64_json") if response_format == "url" and image_url: print(f"MEDIA:{image_url}") continue # Save mode: decode b64 or fallback to url download if not image_b64 and image_url: # API returned url even though we asked for b64 — download it ts = datetime.now().strftime("%Y%m%d-%H%M%S") fn = filename or f"{ts}-{slugify(prompt)}{f'-{idx+1}' if count > 1 else ''}.png" fp = out_dir / fn urllib.request.urlretrieve(image_url, fp) elif image_b64: ts = datetime.now().strftime("%Y%m%d-%H%M%S") fn = filename or f"{ts}-{slugify(prompt)}{f'-{idx+1}' if count > 1 else ''}.png" fp = out_dir / fn fp.write_bytes(base64.b64decode(image_b64)) ``` ```python ap.add_argument("--filename", default="", help="Custom output filename.") ``` ### Technical Analysis The value supplied through `--filename` is used directly as a path component. It is not restricted to a basename and is not checked for absolute paths or parent-directory traversal. With `pathlib`, joining `out_dir` to an absolute `fn` discards `out_dir`. A relative filename containing `../` can similarly resolve outside the intended directory. Both `Path.write_bytes` and `urllib.request.urlretrieve` may replace an existing writable file. The file contents are generated-image bytes returned by the configured API. Although this limits precise content control in ordinary use, an attacker who con ...[truncated 1521 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Restrict `--filename` to a single basename: - Reject absolute paths. - Reject `.` and `..` path components. - Reject directory separators for all supported platforms. 2. Construct the destination and resolve it before writing: ```python base = out_dir.resolve() destination = (base / safe_filename).resolve() if destination.parent != base: raise ValueError("Output filename escapes the selected directory") ``` 3. If subdirectories are intentionally supported, verify with `destination.is_relative_to(base)` rather than requiring the direct parent to equal `base`. 4. Apply a conservative filename-character allowlist and impose a length limit. 5. Avoid silently replacing existing files. Use exclusive file creation, generate a unique name, or require an explicit overwrite flag. 6. Apply the same validated destination logic to both base64 writes and URL downloads. 7. Consider enforcing an expected image extension after validating the decoded content. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (11)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The documented purpose understates actual behavior: the skill can perform image edits, fetch remote input images, and save files in ways that contradict the stated default behavior. This mismatch is dangerous because operators may approve or invoke the skill under false assumptions, leading to unexpected external data transmission, local file creation, and broader attack surface than disclosed.

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill declares no explicit tool scope even though its documented behavior requires environment access, network access, and file writes. That is dangerous because consumers and enforcement layers cannot clearly constrain what the skill is allowed to do, increasing the chance of overbroad execution and abuse if the implementation changes or is invoked unexpectedly.

External Transmission

Medium
Category
Data Exfiltration
Content
args = ap.parse_args()

    api_key = os.environ.get("VVMAI_API_KEY", "").strip()
    base_url = os.environ.get("VVMAI_BASE_URL", "https://api.vvmai.com/v1").strip()
    if not api_key:
        print("Error: VVMAI_API_KEY not set.", file=sys.stderr); sys.exit(1)
    if not base_url:
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
args = ap.parse_args()

    api_key = os.environ.get("VVMAI_API_KEY", "").strip()
    base_url = os.environ.get("VVMAI_BASE_URL", "https://api.vvmai.com/v1").strip()
    if not api_key:
        print("Error: VVMAI_API_KEY not set.", file=sys.stderr); sys.exit(1)
    if not base_url:
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
args = ap.parse_args()

    api_key = os.environ.get("VVMAI_API_KEY", "").strip()
    base_url = os.environ.get("VVMAI_BASE_URL", "https://api.vvmai.com/v1").strip()
    if not api_key:
        print("Error: VVMAI_API_KEY not set.", file=sys.stderr); sys.exit(1)
    if not base_url:
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
args = ap.parse_args()

    api_key = os.environ.get("VVMAI_API_KEY", "").strip()
    base_url = os.environ.get("VVMAI_BASE_URL", "https://api.vvmai.com/v1").strip()
    if not api_key:
        print("Error: VVMAI_API_KEY not set.", file=sys.stderr); sys.exit(1)
    if not base_url:
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Description-Behavior Mismatch

Medium
Confidence
97% confidence
Finding
The code forces local saving for any model whose name starts with 'gpt-image' by setting should_save = args.save or args.oss or is_gpt_image, which contradicts the stated behavior that images are not saved locally by default. This can cause unexpected persistence of generated content on disk, creating privacy and data-handling risks if users believe outputs will only be returned as URLs.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
When --input is used, the script uploads the provided local image to an external VVMAI endpoint without an explicit runtime warning or confirmation that user content is being transmitted off-host. In a skill context, users may supply sensitive images and assume local-only processing, making undisclosed third-party transmission a meaningful privacy issue.

Missing User Warnings

Low
Confidence
91% confidence
Finding
The README explicitly documents that generated images may be written to local disk via `--save`, `--oss`, and always for `gpt-image` models, but it does not clearly warn users at the point of use that local files will be created. In an agent context, silent or insufficiently disclosed file writes can surprise users, create persistence of sensitive/generated content, and increase the risk of unintended storage on shared systems.

Intent-Code Divergence

Low
Confidence
76% confidence
Finding
The top-level docstring frames the script as an image generation and editing client against VVMAI endpoints, but the implementation also downloads remote input images and persists output images to local filesystem paths. This is not merely omitted detail in one place: the docstring presents the script's behavior in a narrower way than the implemented local file handling.

Missing User Warnings

Low
Confidence
86% confidence
Finding
Prompts are sent to a remote API as part of normal service operation, but the script does not explicitly disclose this data sharing at runtime. This is a lower-severity privacy/transparency issue because prompts may contain sensitive text even though remote transmission is expected for hosted image generation.

Static analysis

No suspicious patterns detected.