Back to skill

Security audit

Banana Claws

Security checks for vulnerabilities and agentic risk

Overview

This image-generation skill is mostly purpose-aligned, but it needs review because its background queue can write outside intended paths and terminate local processes based on weak queue metadata.

Review before installing. Use it only in a dedicated workspace and unprivileged account, avoid shared or attacker-writable queue directories, keep request IDs to simple letters/numbers/dashes, do not submit sensitive prompts or images, and periodically clear queue/provider-response files if you do not want that content retained.

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

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/queue_and_return.py:22
Finding
Unverified Persisted PIDs Can Cause Termination of Unrelated Processes<![CDATA[ ## Vulnerability Details **File Location**: `scripts/queue_and_return.py`, lines 22–44 **Vulnerability Type**: Trust of mutable process identifiers and unsafe process termination **Risk Level**: Medium ### Vulnerable Code ```python def _active_workers(handoff_dir: pathlib.Path, stale_seconds: int) -> tuple[int, list[dict]]: now = int(time.time()) active: list[dict] = [] for f in sorted(handoff_dir.glob('*.json')): try: row = json.loads(f.read_text()) except Exception: continue pid = row.get('worker_pid') started = int(row.get('worker_started_at_s') or 0) if not isinstance(pid, int) or pid <= 0: continue alive = _is_pid_alive(pid) stale = bool(started and (now - started > stale_seconds)) if stale and alive: try: os.kill(pid, signal.SIGTERM) time.sleep(0.2) if _is_pid_alive(pid): os.kill(pid, signal.SIGKILL) except OSError: pass alive = _is_pid_alive(pid) row['orphan_cleanup'] = {'stale_seconds': stale_seconds, 'cleaned_at_s': now, 'terminated': not alive} f.write_text(json.dumps(row, ensure_ascii=False, indent=2) + '\n') if alive: active.append({'pid': pid, 'request_id': row.get('request_id'), 'file': str(f)}) return len(active), active ``` ### Technical Analysis The background-worker cleanup logic treats every JSON file in the configurable handoff directory as authoritative process state. It validates only that `worker_pid` is a positive integer and that a process with that PID is alive. It does not verify: - That the PID belongs to a process originally launched by this Skill. - That the executable is Python or the expected `run_image_queue.py` worker. - That the process start time matches the recorded worker start time. - That the process belongs to the expected queue or ...[truncated 1967 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. **Verify process identity before sending signals** - Read `/proc/<pid>/cmdline` and verify that it identifies the expected Python interpreter and `run_image_queue.py`. - Verify that command-line arguments contain the expected queue directory and request ID. - Compare the process start time with a start time recorded when the worker was launched. 2. **Use stronger worker identity** - Generate an unpredictable worker token when launching a worker. - Pass the token to the worker and record it in a protected state file. - Require all identity checks to succeed before treating a process as owned by the Skill. 3. **Protect state files** - Create queue and handoff directories with owner-only permissions where possible. - Reject handoff files not owned by the expected user. - Use atomic file creation and updates. - Do not trust state files from shared or attacker-writable directories. 4. **Handle PID reuse** - Store both the PID and operating-system process start time. - Treat a mismatch as a stale record that must be removed without signaling the current process. 5. **Reduce termination aggressiveness** - Avoid automatically escalating to `SIGKILL` after only 0.2 seconds. - Provide a reasonable shutdown timeout. - If worker ownership cannot be conclusively verified, remove or quarantine the record without signaling the process. 6. **Run with minimum privileges** - Never run the queue launcher as root or under an account that owns unrelated critical services. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/queue_and_return.py:133
Finding
Unsanitized Request ID Allows Handoff File Path Traversal<![CDATA[ ## Vulnerability Details **File Location**: `scripts/queue_and_return.py`, lines 133–161 **Vulnerability Type**: Path traversal and attacker-controlled file creation or truncation **Risk Level**: Medium ### Vulnerable Code ```python pid = None worker_log = handoff_dir / f'{args.request_id}.worker.log' active_count, active_workers = _active_workers(handoff_dir, stale_seconds=args.orphan_timeout_sec) worker_state = 'spawned' worker_skip_reason = '' if active_count >= args.max_background_workers: worker_state = 'skipped' worker_skip_reason = f'active_workers={active_count} >= max_background_workers={args.max_background_workers}' elif not args.dry_run_worker: with worker_log.open('ab') as logf: proc = subprocess.Popen(worker_cmd, stdout=logf, stderr=logf, stdin=subprocess.DEVNULL, start_new_session=True) pid = proc.pid handoff = { 'request_id': args.request_id, 'queued_at_ms': int(time.time() * 1000), 'handoff_mode': 'background', 'worker_cmd': worker_cmd, 'worker_pid': pid, 'worker_log': str(worker_log), 'worker_started_at_s': int(time.time()) if pid else None, 'worker_state': worker_state, 'worker_skip_reason': worker_skip_reason, 'active_workers_before_spawn': active_workers, 'max_background_workers': args.max_background_workers, 'orphan_timeout_sec': args.orphan_timeout_sec, 'enqueue_stdout': cp.stdout.strip(), } handoff_path = handoff_dir / f'{args.request_id}.json' handoff_path.write_text(json.dumps(handoff, ensure_ascii=False, indent=2) + '\n') ``` ### Technical Analysis The required `--request-id` argument is inserted directly into two filesystem paths without validation: - `<handoff-dir>/<request-id>.worker.log` - `<handoff-dir>/<request-id>.json` A request ID containing `../` path components can escape the intended handoff directory. An absolute request ID can also cause `pathlib` to discard the preceding `handoff_dir` component after the filename suffix is app ...[truncated 2041 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. **Validate request IDs using an allowlist** - Accept only characters needed for identifiers, for example: ```python if not re.fullmatch(r'[A-Za-z0-9_-]{1,128}', args.request_id): raise ValueError('Invalid request ID') ``` - Explicitly reject path separators, `.` and `..` path components, control characters, and absolute paths. 2. **Avoid using raw external identifiers as filenames** - Derive the filename from a SHA-256 digest of the request ID. - Preserve the original request ID only inside the JSON record. 3. **Enforce directory containment** - Resolve the final path and verify that it is beneath the resolved handoff directory before opening it: ```python root = handoff_dir.resolve() candidate = (root / filename).resolve() candidate.relative_to(root) ``` 4. **Create files safely** - Use exclusive creation where overwriting is unnecessary. - Set restrictive file permissions. - Write JSON to a temporary file within the same protected directory and atomically rename it into place. 5. **Apply the same validation consistently** - Validate request IDs before they are forwarded to `enqueue_variants.py` and `run_image_queue.py`. - Review every location where request IDs or prefixes are incorporated into paths. 6. **Run with minimum filesystem privileges** - Restrict the Skill account to its workspace and generated-output directories. - Avoid running this workflow as root or from directories containing sensitive writable configuration files. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • System Prompt LeakageDirect Leakage, Indirect Extraction, Tool-Based Exfiltration
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (20)

Tainted flow: 'headers' from os.getenv (line 205, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Content
'Content-Type': 'application/json',
    }

    r = requests.post('https://openrouter.ai/api/v1/chat/completions', headers=headers, data=json.dumps(payload), timeout=180)
    if r.status_code >= 300:
        print(f'Generation failed: {r.status_code} {r.text[:500]}', file=sys.stderr)
        return 1
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Direct Prompt Extraction

High
Category
System Prompt Leakage
Content
--model openai/gpt-5-image
--model openai/gpt-5-image-mini
--image-size low|medium|high
--clarify-hints      # print prompt-quality hints to stderr
--strict-clarify     # fail fast when prompt appears underspecified
--baseline-image ./path/to/reference.png
--baseline-source-kind current_attachment|reply_attachment|explicit_path_or_url
Confidence
85% confidence
Finding
Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Direct Prompt Extraction

High
Category
System Prompt Leakage
Content
p.add_argument('--out', required=True)
    p.add_argument('--model', default='openai/gpt-5-image')
    p.add_argument('--image-size', choices=['low', 'medium', 'high'], default='', help='Model-dependent quality/size tier for iterative vs final passes')
    p.add_argument('--clarify-hints', action='store_true', help='Print prompt-clarification hints before generation')
    p.add_argument('--strict-clarify', action='store_true', help='Fail fast if prompt appears ambiguous for production-style tasks')
    p.add_argument('--baseline-image', default='', help='Path/URL to baseline image for locked variants')
    p.add_argument('--baseline-source-kind', choices=['current_attachment', 'reply_attachment', 'explicit_path_or_url'], default='', help='How baseline was resolved by caller')
Confidence
85% confidence
Finding
Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Lp3

Medium
Category
MCP Least Privilege
Confidence
96% confidence
Finding
The skill explicitly instructs execution of local Python scripts, file reads/writes, environment-variable access, and outbound network requests to OpenRouter, but it does not declare any permissions or allowed-tools scope. This creates an authorization gap: a host agent may invoke the skill without clear least-privilege boundaries, increasing the chance of unintended shell, filesystem, or network use.

External Transmission

Medium
Category
Data Exfiltration
Content
'Content-Type': 'application/json',
    }

    r = requests.post('https://openrouter.ai/api/v1/chat/completions', headers=headers, data=json.dumps(payload), timeout=180)
    if r.status_code >= 300:
        print(f'Generation failed: {r.status_code} {r.text[:500]}', file=sys.stderr)
        return 1
Confidence
70% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
Saving the full provider response JSON can expose prompt text, generation metadata, and possibly attachment-related context to local storage without strong guardrails. In an agent environment, this increases the risk of inadvertent retention, later exfiltration, or cross-task disclosure of sensitive user data.

Tainted flow: 'data' from requests.post (line 215, network input) → pathlib.Path.write_text (file write)

Medium
Category
Data Flow
Content
if args.save_response_json:
        save_path = pathlib.Path(args.save_response_json)
        save_path.parent.mkdir(parents=True, exist_ok=True)
        save_path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + '\n')

    choices = data.get('choices') or []
    if not choices:
Confidence
88% confidence
Finding
The script can persist the full provider response JSON to an arbitrary caller-supplied path. That response may contain sensitive prompt content, metadata, and potentially returned URLs or model details, so writing it to disk without path restrictions or minimization creates an avoidable data exposure risk in an agent workflow.

Tainted flow: 'b64' from requests.post (line 245, network input) → pathlib.Path.write_bytes (file write)

Medium
Category
Data Flow
Content
if data_url.startswith('data:image') and 'base64,' in data_url:
        b64 = data_url.split('base64,', 1)[1]
        out.write_bytes(base64.b64decode(b64))
    elif data_url.startswith('http://') or data_url.startswith('https://'):
        img = requests.get(data_url, timeout=180)
        img.raise_for_status()
Confidence
91% confidence
Finding
The script decodes base64 image data from a remote provider response and writes it directly to a caller-chosen output path without validating content type, size, or path safety. In an automation setting, this can enable disk overwrite, oversized file writes, or storage of unexpected content if the provider or upstream response is compromised.

Tainted flow: 'data_url' from requests.post (line 118, network input) → requests.get (network output)

Medium
Category
Data Flow
Content
b64 = data_url.split('base64,', 1)[1]
        out.write_bytes(base64.b64decode(b64))
    elif data_url.startswith('http://') or data_url.startswith('https://'):
        img = requests.get(data_url, timeout=180)
        img.raise_for_status()
        out.write_bytes(img.content)
    else:
Confidence
95% confidence
Finding
The code performs a second network request to a URL taken directly from the provider response. This creates an SSRF-style primitive: if the provider response is malicious or compromised, the agent could be induced to fetch arbitrary internal or external resources reachable from its network environment.

Tainted flow: 'img' from requests.get (line 248, network input) → pathlib.Path.write_bytes (file write)

Medium
Category
Data Flow
Content
elif data_url.startswith('http://') or data_url.startswith('https://'):
        img = requests.get(data_url, timeout=180)
        img.raise_for_status()
        out.write_bytes(img.content)
    else:
        print('Unknown image URL format', file=sys.stderr)
        return 1
Confidence
93% confidence
Finding
Content fetched from a remote URL is written directly to disk without validation. Combined with the previous untrusted fetch, this can store arbitrary attacker-influenced bytes, leading to unwanted file creation, overwrite, or persistence of non-image content in local agent storage.

Context-Inappropriate Capability

Medium
Confidence
89% confidence
Finding
The script reads PID values from JSON files in the handoff directory and can send SIGTERM/SIGKILL to any live process matching those PIDs once deemed stale. If an attacker or another local actor can modify or plant handoff files, they may be able to induce termination of unrelated local processes, causing denial of service or unsafe interference with other workloads.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
The file automatically spawns detached background workers and elsewhere can terminate stale ones without any explicit user-facing confirmation in this code path. In an agent skill context, that means a simple image-generation request can trigger persistent local process management behavior beyond the user's likely expectation, increasing the blast radius if the queue location or metadata is manipulated.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
worker_skip_reason = f'active_workers={active_count} >= max_background_workers={args.max_background_workers}'
    elif not args.dry_run_worker:
        with worker_log.open('ab') as logf:
            proc = subprocess.Popen(worker_cmd, stdout=logf, stderr=logf, stdin=subprocess.DEVNULL, start_new_session=True)
            pid = proc.pid

    handoff = {
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
This code prepares and executes a generator command that sends the job prompt and optional baseline image data to an external model provider, and it also saves the provider response JSON. Although the behavior is inferable from argument names, this file provides no confirmation prompt, print/log disclosure, or inline warning that user content may be transmitted externally and recorded.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
if job.get('confirm_external_upload'):
            cmd.append('--confirm-external-upload')

        cp = subprocess.run(cmd, capture_output=True, text=True)
        job['finished_at_ms'] = _now_ms()
        job['exit_code'] = cp.returncode
        job['stdout'] = (cp.stdout or '').strip()
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
if job.get('confirm_external_upload'):
            cmd.append('--confirm-external-upload')

        cp = subprocess.run(cmd, capture_output=True, text=True)
        job['finished_at_ms'] = _now_ms()
        job['exit_code'] = cp.returncode
        job['stdout'] = (cp.stdout or '').strip()
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
if job.get('confirm_external_upload'):
            cmd.append('--confirm-external-upload')

        cp = subprocess.run(cmd, capture_output=True, text=True)
        job['finished_at_ms'] = _now_ms()
        job['exit_code'] = cp.returncode
        job['stdout'] = (cp.stdout or '').strip()
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Low
Confidence
76% confidence
Finding
The script creates parent directories and writes the generated image bytes to the output path, which is a file write operation. There is no confirmation prompt, visible log/print statement, or explanatory comment/docstring around this persistence step, so users invoking the script receive no explicit disclosure that local files will be created or overwritten.

Missing User Warnings

Low
Confidence
78% confidence
Finding
The script inspects the presence of OPENROUTER_API_KEY, which is a credential-like environment variable. While the script prints check results, it does not include any comment, docstring, or user-facing warning explaining that it accesses an API key environment variable or how that credential will be handled.

Missing User Warnings

Low
Confidence
83% confidence
Finding
The script renames pending job files into processing, updates their contents, and later writes result or failure records and deletes the processing copy. These are user-data-affecting file mutations, but the file contains no user-facing disclosure beyond operational code and no inline warning describing that queued job JSON will be modified and relocated.

Static analysis

No suspicious patterns detected.