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

Content
View full analysis

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

Content
View full analysis

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

Content
View full analysis

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

Content
View full analysis

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
View 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.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

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.

Content

No source excerpt is available for this finding.

Missing User Warnings

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

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

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

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 · README.md (reported line 21)May include surrounding context.

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

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 · SKILL.md (reported line 68)May include surrounding context.

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

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/gen.py (reported line 197)May include surrounding context.

python
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:

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/gen.py (reported line 201)May include surrounding context.

python
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:

Intent-Code Divergence

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

Content

No source excerpt is available for this finding.

Missing User Warnings

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

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.