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

Content
View full analysis
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
View remediation
/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

Content
View full analysis
= 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: - `/.worker.log` - `/.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
View remediation
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
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/generate_image.py (reported line 210)May include surrounding context.

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

Direct Prompt Extraction

High
Category
System Prompt Leakage
Confidence
85% confidence
Finding

Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Content

Scanner excerpt · SKILL.md (reported line 55)May include surrounding context.

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

Direct Prompt Extraction

High
Category
System Prompt Leakage
Confidence
85% confidence
Finding

Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Content

Scanner excerpt · scripts/generate_image.py (reported line 128)May include surrounding context.

python
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')

Undeclared Tool Scope

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.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
70% 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/generate_image.py (reported line 210)May include surrounding context.

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

Missing User Warnings

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

Content

No source excerpt is available for this finding.

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

Medium
Category
Data Flow
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.

Content

Scanner excerpt · scripts/generate_image.py (reported line 220)May include surrounding context.

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

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

Medium
Category
Data Flow
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.

Content

Scanner excerpt · scripts/generate_image.py (reported line 246)May include surrounding context.

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

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

Medium
Category
Data Flow
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.

Content

Scanner excerpt · scripts/generate_image.py (reported line 248)May include surrounding context.

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

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

Medium
Category
Data Flow
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.

Content

Scanner excerpt · scripts/generate_image.py (reported line 250)May include surrounding context.

python
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

Context-Inappropriate Capability

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

Content

No source excerpt is available for this finding.

Missing User Warnings

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

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/queue_and_return.py (reported line 142)May include surrounding context.

python
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 = {

Missing User Warnings

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

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/enqueue_variants.py (reported line 98)May include surrounding context.

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

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/queue_and_return.py (reported line 115)May include surrounding context.

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

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/run_image_queue.py (reported line 96)May include surrounding context.

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

Missing User Warnings

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

Content

No source excerpt is available for this finding.

Missing User Warnings

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

Content

No source excerpt is available for this finding.

Missing User Warnings

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

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.