Back to skill

Security audit

nano-banana、gpt-image

Security checks for vulnerabilities and agentic risk

Overview

This image-generation skill is mostly coherent, but it has under-disclosed edit features that can upload local files or fetched URLs to a configurable API endpoint.

Review before installing. Use it only if you are comfortable sending prompts, selected input images, and the VAPI API key to the configured VAPI endpoint. Avoid using --input with sensitive local paths or untrusted URLs, and keep VAPI_BASE_URL on a trusted HTTPS endpoint.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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 (4)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/gen.py:115
Finding
Undocumented Arbitrary Local File Upload to a Configurable Remote Endpoint## Vulnerability Details **File Location**: `scripts/gen.py:115-141, 203-224` **Vulnerability Type**: Arbitrary local file disclosure through undocumented image-editing functionality **Risk Level**: High ### Vulnerable Code ```python # Image file img_data = image_path.read_bytes() img_mime = "image/jpeg" if image_path.suffix.lower() in (".jpg", ".jpeg") else "image/png" parts.append( ( f"--{boundary}\r\n" f'Content-Disposition: form-data; name="image"; filename="{image_path.name}"\r\n' f"Content-Type: {img_mime}\r\n\r\n" ).encode("utf-8") + img_data + b"\r\n" ) parts.append(f"--{boundary}--\r\n".encode("utf-8")) body = b"".join(parts) req = urllib.request.Request( url, method="POST", headers={ "Authorization": f"Bearer {api_key}", "Content-Type": f"multipart/form-data; boundary={boundary}", }, data=body, ) ``` ```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) print(f"Editing image with model={args.model} format={response_format}...", file=sys.stderr) response = request_edit( base_url=base_url, api_key=api_key, prompt=args.prompt, model=args.model, image_path=local_input, response_format=response_format, size=args.size, aspect_ratio=args.aspect_ratio, count=args.count, ) ``` ### Technical Analysis The `--input` argument accepts an arbitrary local path. The selected file is read in full with `Path.read_bytes()` and included in a multipart request to `${VAPI_BASE_URL}/images/edits`. The implementation does not: - Restrict files to an approved media directory. - Verify that the selected file is an image by inspe ...[truncated 1942 chars]
Remediation
## Remediation Suggestions 1. Remove edit mode if it is not part of the intended and documented Skill functionality. 2. If editing is required, document clearly that the selected image and prompt are transmitted to the configured API provider. 3. Require explicit user approval immediately before uploading a local file. 4. Resolve the path with `Path.resolve()` and constrain it to one or more user-approved media directories. 5. Reject symlinks or validate their fully resolved targets to prevent directory-boundary bypass. 6. Validate image content using a trusted image decoder rather than relying on the filename extension. 7. Allow only explicitly supported image formats and enforce conservative file-size and pixel-count limits. 8. Reject sensitive locations, device files, sockets, and other non-regular files. 9. Display the resolved destination host and local source path before transmission. 10. Keep edit functionality separate from generation functionality so generation does not require broad local-file access.

T09 · Insecure Skill Coding Practices

Error
Location
scripts/gen.py:33
Finding
Server-Side Request Forgery Through Unrestricted Input Image URLs## Vulnerability Details **File Location**: `scripts/gen.py:33-45, 203-207` **Vulnerability Type**: Unrestricted server-side URL retrieval **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 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 A caller-controlled `--input` value beginning with `http` is fetched with `urllib.request.urlopen()`. The implementation applies no destination restrictions and does not validate: - URL schemes precisely. - Destination hostnames or resolved IP addresses. - Loopback, private, link-local, multicast, or reserved network ranges. - Cloud instance metadata addresses. - Redirect destinations. - Response content type. - Response size. The fixed timeout limits request duration but does not prevent access to internal resources or unbounded disk consumption before the timeout. The downloaded response is subsequently passed to the image-editing request, creating a potential external forwarding channel for content retrieved from internal services. ### Attack Path 1. An attacker causes the Skill to receive an attacker-selected `--input` URL. 2. The URL targets ...[truncated 1367 chars]
Remediation
## Remediation Suggestions 1. Permit only explicitly parsed `https` URLs using `urllib.parse.urlsplit()`. 2. Resolve the hostname and reject every address in loopback, private, link-local, multicast, unspecified, reserved, and other non-public ranges. 3. Block known cloud metadata destinations, including link-local metadata addresses. 4. Disable automatic redirects or validate the scheme, hostname, port, and resolved addresses after every redirect. 5. Prefer an explicit allowlist of trusted image-hosting domains when operationally possible. 6. Permit only standard HTTPS ports unless additional ports are explicitly required. 7. Enforce a conservative maximum response size while streaming. 8. Validate the response `Content-Type` and decode the downloaded data with a trusted image library before uploading it. 9. Apply connection and read timeouts separately. 10. Consider requiring the user to download or select the image through a controlled media tool instead of allowing arbitrary URLs.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/gen.py:189
Finding
API Credentials and User Content Can Be Transmitted Over Plaintext HTTP## Vulnerability Details **File Location**: `scripts/gen.py:64-70, 133-141, 189-192` **Vulnerability Type**: Missing transport and destination validation for sensitive API requests **Risk Level**: Medium ### Vulnerable Code ```python 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, ) try: with urllib.request.urlopen(req, timeout=120) as resp: return json.loads(resp.read().decode("utf-8")) ``` ```python req = urllib.request.Request( url, method="POST", headers={ "Authorization": f"Bearer {api_key}", "Content-Type": f"multipart/form-data; boundary={boundary}", }, data=body, ) try: with urllib.request.urlopen(req, timeout=180) as resp: return json.loads(resp.read().decode("utf-8")) ``` ```python api_key = os.environ.get("VAPI_API_KEY", "").strip() base_url = os.environ.get("VAPI_BASE_URL", "https://api.v3.cm/v1").strip() if not api_key: print("Error: VAPI_API_KEY not set.", file=sys.stderr); sys.exit(1) ``` ### Technical Analysis The default base URL uses HTTPS, but `VAPI_BASE_URL` is accepted without validating its scheme, host, port, path, or ownership. Both generation and editing requests attach the bearer API key to the resulting URL. If the environment variable contains a plaintext HTTP URL, the API key, prompt, model parameters, and uploaded image data are sent without transport encryption. If it points to an unintended HTTPS host, encryption protects the connection but still delivers all request data and the bearer credential to that host. Configuration through an environment variable can be legitimate, but sensitive credentials should not be attached until the destination satisfies strict trust and transport requirements. ### Attack Path 1. An attacker, unsafe d ...[truncated 1113 chars]
Remediation
## Remediation Suggestions 1. Parse `VAPI_BASE_URL` and reject every scheme other than `https`. 2. Allowlist the official API hostname by default. 3. If custom endpoints are necessary, require explicit opt-in and clearly identify the destination before sending credentials or content. 4. Reject URLs containing embedded user information, fragments, unexpected query strings, or unsupported ports. 5. Normalize and validate the final request URL after joining endpoint paths. 6. Do not forward the primary API credential to arbitrary custom hosts; use separately scoped credentials for custom providers. 7. Apply certificate verification using the platform trust store and do not introduce certificate-validation bypasses. 8. Scope API keys to the minimum required operations and configure rotation, usage limits, and monitoring.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/gen.py:33
Finding
Remote Input Files Are Left Persistently in Temporary Storage## Vulnerability Details **File Location**: `scripts/gen.py:33-45, 203-207` **Vulnerability Type**: Unsafe temporary-file lifecycle and unbounded temporary storage use **Risk Level**: Medium ### 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 input_path.startswith("http"): print(f"Downloading input image...", file=sys.stderr) local_input = download_to_temp(input_path) ``` ### Technical Analysis `NamedTemporaryFile(delete=False)` creates a persistent temporary file. The returned path is never deleted after a successful edit request and is also not cleaned up when later processing fails. The original temporary-file object remains open while the same path is reopened for writing. Although this commonly works on Unix-like systems, it is an unnecessary lifecycle error and may behave inconsistently on platforms with stricter file-sharing semantics. In addition, the download loop does not limit the total number of bytes written. Repeated executions or a single oversized response can consume substantial temporary storage. ### Attack Path 1. A caller supplies a remote URL through `--input`. 2. The script creates a temporary file with deletion disabled. 3. It streams the response into that file without a maximum byte count. 4. The file is used for the edit request. 5. Execution finishes or encounters ...[truncated 689 chars]
Remediation
## Remediation Suggestions 1. Use `tempfile.TemporaryDirectory()` or another context-managed temporary resource. 2. Delete the downloaded file in a `finally` block after the edit request completes or fails. 3. Close the original temporary-file handle before reopening or processing the path. 4. Track the cumulative number of downloaded bytes and abort when a conservative limit is exceeded. 5. Check `Content-Length` when present, while still enforcing the streaming limit because the header may be absent or false. 6. Preserve restrictive filesystem permissions and avoid exposing temporary paths in normal output. 7. Periodically clean abandoned files as defense in depth, while retaining deterministic per-execution cleanup.
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 (10)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The skill's declared behavior says images are not saved locally by default and presents itself as image generation only, but the analysis indicates additional capabilities including image editing, remote input-image download, forced local saving for some models, and extra save destinations. This mismatch prevents informed consent and can cause unexpected transmission, modification, or persistence of user content on disk or from remote sources.

Lp3

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding
The skill declares access to environment variables and, by description, supports network access and local file writes, but it does not explicitly scope or constrain those capabilities with permissions or allowed-tools metadata. That omission weakens sandboxing and reviewability, making it easier for a skill to overreach or for users to misunderstand what resources it may access.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The skill description and usage text do not clearly warn that user prompts are sent to a third-party external API for processing. Users may provide sensitive prompts under the assumption processing is local, creating privacy and data-handling risk that is especially relevant for image prompts that may contain confidential or personal material.

Description-Behavior Mismatch

Medium
Confidence
97% confidence
Finding
The manifest description says the skill generates images via the Images API, with no mention of editing existing images. The code and module docstring explicitly support edit mode when --input is provided and send requests to /images/edits, which is a materially broader capability than described.

External Transmission

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

    api_key = os.environ.get("VAPI_API_KEY", "").strip()
    base_url = os.environ.get("VAPI_BASE_URL", "https://api.v3.cm/v1").strip()
    if not api_key:
        print("Error: VAPI_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("VAPI_API_KEY", "").strip()
    base_url = os.environ.get("VAPI_BASE_URL", "https://api.v3.cm/v1").strip()
    if not api_key:
        print("Error: VAPI_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("VAPI_API_KEY", "").strip()
    base_url = os.environ.get("VAPI_BASE_URL", "https://api.v3.cm/v1").strip()
    if not api_key:
        print("Error: VAPI_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("VAPI_API_KEY", "").strip()
    base_url = os.environ.get("VAPI_BASE_URL", "https://api.v3.cm/v1").strip()
    if not api_key:
        print("Error: VAPI_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.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The documented intent states that local saving should occur only when the user uses --save. However, the code sets should_save to true whenever the model name starts with gpt-image, causing b64 output and local file writes without requiring --save.

Missing User Warnings

Low
Confidence
86% confidence
Finding
This markdown file explicitly documents that images can be saved locally, including default save behaviors for certain models, but it does not include any caution that local files will be written to the user's filesystem. For markdown files, SQP-2 applies when behaviors affecting user data or system integrity are described without warnings, and local file creation qualifies as a user-data-impacting action.

Static analysis

No suspicious patterns detected.