Back to skill

Security audit

Openclaw Comfyui Api Runner

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its ComfyUI image-generation purpose, but it under-protects saved credentials and locally served generated files.

Review before installing. Use only trusted ComfyUI servers, avoid saving API keys or passwords as profiles, prefer environment-only credentials, use HTTPS for any non-local server, and do not run the generated-media viewer on an untrusted network unless it is changed to bind only to localhost and require authentication.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/comfy_client.py:193
Finding

Stored ComfyUI Credentials Are Disclosed Through Agent Output

Content
View full analysis
dict[str, Any]: cfg = _load_user_config() profiles = cfg.get("profiles") if isinstance(cfg.get("profiles"), dict) else {} servers = cfg.get("servers") if isinstance(cfg.get("servers"), list) else [] return { "active_profile": cfg.get("active_profile"), "default_server": cfg.get("default_server"), "default_workflow": cfg.get("default_workflow"), "profiles": {name: profile for name, profile in sorted(profiles.items()) if isinstance(profile, dict)}, "servers": [server for server in servers if isinstance(server, dict)], "config_path": str(USER_CONFIG_PATH), } ``` ```python def cmd_list_profiles() -> int: print(json.dumps({"status": "profiles", **list_profiles()})) return 0 def cmd_save_profile(args: argparse.Namespace) -> int: cfg = resolve_config() profile = { "base_url": cfg["base_url"], "port": cfg["port"], } if cfg.get("api_key"): profile["api_key"] = cfg["api_key"] if cfg.get("username"): profile["username"] = cfg["username"] if cfg.get("password"): profile["password"] = cfg["password"] out = save_profile(args.save_profile, profile, set_active=not args.no_activate) print(json.dumps({"status": "profiles", **out})) return 0 ``` ### Technical Analysis Saved profile objects may contain `api_key`, `username`, and `password`. The `list_profiles()` function returns those objects without redaction. Both profile listing and profile saving serialize the returned structure to stdout. In an Agent environment, stdout is returned to the caller and may also be retained in conversation history, execution logs, telemetry, or debugging records. Consequently, a profile-management operati ...[truncated 1022 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/comfy_client.py:339
Finding

Authorization Credentials Can Be Forwarded to an Unvalidated Output URL

Content
View full analysis
dict[str, Any]: base = base_url.rstrip("/") for prompt_id, data in history.items(): if not isinstance(data, dict): continue outputs = data.get("outputs") or {} for node_id, node_out in outputs.items(): if not isinstance(node_out, dict): continue for img in node_out.get("images") or []: if not isinstance(img, dict) or "filename" not in img: continue if img.get("view_url"): continue fn = img["filename"] subfolder = img.get("subfolder", "") img_type = img.get("type", "output") q = f"filename={fn}&type={img_type}&subfolder={subfolder}" if prompt_id: q += f"&prompt_id={prompt_id}" img["view_url"] = f"{base}/view?{q}" return history ``` ```python for img in images: view_url = img.get("view_url") if not view_url: continue local_name = _safe_output_name(prompt_id, img) local_path = GENERATED_DIR / local_name try: body = fetch_url(view_url, skill_root=skill_root) local_path.write_bytes(body) except Exception: continue ``` ```python def fetch_url(url: str, skill_root: Path | None = None) -> bytes: cfg = resolve_config(skill_root) req = urllib.request.Request(url, method="GET") for name, value in _build_headers(cfg.get("api_key")).items(): req.add_header(name, value) _install_basic_auth(req, cfg.get("username"), cfg.get("password")) with urllib.request.urlopen(req, timeout=TIMEOUT) as resp: return res ...[truncated 1839 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/comfy_client.py:43
Finding

API Keys and Passwords Are Persisted in Plaintext with Unenforced File Permissions

Content
View full analysis
None: path.parent.mkdir(parents=True, exist_ok=True) with open(path, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) f.write("\n") ``` ```python def save_profile(name: str, profile: dict[str, Any], set_active: bool = True) -> dict[str, Any]: if not name or not name.strip(): raise ValueError("Profile name is required") cfg = _load_user_config() profiles = cfg.setdefault("profiles", {}) if not isinstance(profiles, dict): profiles = {} cfg["profiles"] = profiles profiles[name] = { "name": name, "base_url": profile.get("base_url") or DEFAULT_BASE, "port": _sanitize_port(profile.get("port") or DEFAULT_PORT), **({"api_key": profile["api_key"]} if profile.get("api_key") else {}), **({"username": profile["username"]} if profile.get("username") else {}), **({"password": profile["password"]} if profile.get("password") else {}), } servers = cfg.setdefault("servers", []) if not isinstance(servers, list): servers = [] cfg["servers"] = servers servers = [s for s in servers if not (isinstance(s, dict) and str(s.get("name") or "") == name)] servers.append(profiles[name]) cfg["servers"] = servers if set_active: cfg["active_profile"] = name cfg["default_server"] = name _save_json(USER_CONFIG_PATH, cfg) return list_profiles() ``` ### Technical Analysis The profile implementation stores reusable API keys and passwords directly in `~/.config/openclaw/comfyui-runner.json`. `_save_json()` uses a normal text-file write and does not create the file with mode `0600`, check the resulting permissions, or c ...[truncated 1060 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/serve_generated.py:190
Finding

Generated Media Viewer Exposes Files and Deletion Operations on All Network Interfaces

Content
View full analysis
if path.startswith("/files/"): filename = path[7:].lstrip("/").split("/")[0] safe = _safe_path(generated_dir, filename) if safe: self.send_response(200) mime = MIME_TYPES.get(safe.suffix.lower(), "application/octet-stream") self.send_header("Content-Type", mime) self.send_header("Content-Disposition", f'inline; filename="{filename}"') self.end_headers() self.wfile.write(safe.read_bytes()) return self.send_response(404) self.end_headers() return ``` ```python def run_server(port: int, timeout_sec: int) -> None: """Run the HTTP server (blocking). Used by the daemon subprocess.""" generated = SKILL_ROOT / "generated" generated.mkdir(parents=True, exist_ok=True) server = HTTPServer(("0.0.0.0", port), GeneratedHandler) server.generated_dir = generated ``` ### Technical Analysis The viewer advertises a loopback URL to the user, but the server binds to `0.0.0.0`, making it reachable through every network interface allowed by host firewall and container networking rules. No authentication or random access token protects the index, media download, timer-reset, or delete endpoints. The `/delete` endpoint performs a state-changing operation through an unauthenticated GET request. This allows both direct unau ...[truncated 1351 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/comfy_client.py:139
Finding

Authentication Credentials May Be Sent over Unencrypted HTTP Connections

Content
View full analysis
dict[str, str]: headers = { "Content-Type": "application/json", "Accept": "application/json", } if api_key: headers["Authorization"] = f"Bearer {api_key}" return headers def _install_basic_auth(req: urllib.request.Request, username: str | None, password: str | None) -> None: if username and password: creds = base64.b64encode(f"{username}:{password}".encode()).decode() req.add_header("Authorization", f"Basic {creds}") def _request( method: str, path: str, data: dict[str, Any] | None = None, skill_root: Path | None = None, ) -> dict[str, Any] | list[Any]: cfg = resolve_config(skill_root) base = get_base_url(skill_root) url = urljoin(base + "/", path.lstrip("/")) body = json.dumps(data).encode("utf-8") if data is not None else None req = urllib.request.Request(url, data=body, headers=_build_headers(cfg.get("api_key")), method=method) _install_basic_auth(req, cfg.get("username"), cfg.get("password")) try: with urllib.request.urlopen(req, timeout=TIMEOUT) as resp: ``` ### Technical Analysis The connector permits arbitrary HTTP base URLs while attaching Bearer tokens or Basic-auth credentials to requests. Base64 encoding used by Basic authentication is reversible encoding, not encryption. When the configured host is remote and the scheme is HTTP, network observers can capture credentials, workflow prompts, generation metadata, and server responses. Using HTTP for a loopback-only endpoint may be acceptable because traffic does not leave the host. The implementation, however, does not distinguish loopback addresses from remote hosts or warn users before sending credentials over cleartext tran ...[truncated 1178 chars]
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (17)

Tainted flow: 'req' from os.environ.get (line 24, credential/environment) → urllib.request.urlopen (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/serve_status.py (reported line 25)May include surrounding context.

python
reset_url = f"http://127.0.0.1:{port}/reset-timer"
    try:
        req = Request(reset_url)
        with urlopen(req, timeout=TIMEOUT) as _:
            out = {
                "status": "serve_status",
                "running": True,

Lp1

High
Category
MCP Least Privilege
Confidence
89% confidence
Finding

This client performs outbound network requests based on configuration and also includes a generic fetch_url function that can request arbitrary URLs. Undeclared network capability is dangerous in this context because it enables connections beyond narrowly scoped ComfyUI operations, increasing SSRF/exfiltration risk if untrusted input can influence targets.

Content

No source excerpt is available for this finding.

Lp1

High
Category
MCP Least Privilege
Confidence
89% confidence
Finding

This client performs outbound network requests based on configuration and also includes a generic fetch_url function that can request arbitrary URLs. Undeclared network capability is dangerous in this context because it enables connections beyond narrowly scoped ComfyUI operations, increasing SSRF/exfiltration risk if untrusted input can influence targets.

Content

No source excerpt is available for this finding.

Lp1

High
Category
MCP Least Privilege
Confidence
89% confidence
Finding

This client performs outbound network requests based on configuration and also includes a generic fetch_url function that can request arbitrary URLs. Undeclared network capability is dangerous in this context because it enables connections beyond narrowly scoped ComfyUI operations, increasing SSRF/exfiltration risk if untrusted input can influence targets.

Content

No source excerpt is available for this finding.

Missing User Warnings

High
Category
Not specified by scanner
Confidence
95% confidence
Finding

The profile-saving path persists API keys, usernames, and passwords to disk without any visible encryption, redaction, or warning, increasing the chance of credential disclosure through local file access, backups, logs, or accidental sharing. In this skill’s context, stored credentials may grant access to remote ComfyUI servers or adjacent infrastructure, making the exposure more serious than ordinary preference storage.

Content

No source excerpt is available for this finding.

Lp1

High
Category
MCP Least Privilege
Confidence
75% confidence
Finding

The skill uses 'shell' capability that is not listed in its permissions. This may indicate deceptive intent or missing permission declarations.

Content

No source excerpt is available for this finding.

Env Variable Harvesting

High
Category
Data Exfiltration
Confidence
60% confidence
Finding

Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Content

Scanner excerpt · scripts/serve_generated.py (reported line 219)May include surrounding context.

python
return 0

    # Otherwise: spawn daemon subprocess, wait for it to bind, print JSON success, exit so the agent gets immediate response.
    env = {**os.environ, "COMFYUI_SERVE_DAEMON": "1"}
    child = Popen(
        [sys.executable, __file__],
        cwd=str(SKILL_ROOT),

Session Persistence

Medium
Category
Rogue Agent
Confidence
60% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

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

md
If a keyword matches exactly one saved workflow, you can pass the keyword instead of the template id.

## Output
- `poll` / `watch` write downloaded files to `generated/`
- return JSON includes `view_url`, `local_path`, and `outputs[]`
- image files can be sent directly into Discord or another channel if you want delivery there
- server profiles are remembered in `~/.config/openclaw/comfyui-runner.json`

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

save_profile writes API keys and passwords into a JSON config file without any visible safeguards in this code, such as restrictive permissions or secure secret storage. Persisting plaintext credentials increases the chance of local credential disclosure through other processes, backups, shared accounts, or accidental file exposure.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The fetch_url helper accepts any URL and attaches configured API key or Basic Auth credentials to the outbound request. In a skill presented as a safe ComfyUI connector, this broad capability materially expands the attack surface: if an attacker can influence the URL, they may trigger SSRF to internal services or credential leakage to attacker-controlled hosts.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

A generic outbound downloader is not justified by the stated purpose of safe image generation and undermines the trust boundary implied by the skill description. Because the function can fetch arbitrary remote content with attached authentication headers, it can be abused for internal network probing, data access, or secret exfiltration.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The poll/watch flow automatically creates the generated directory and writes every downloaded image to local storage, but this side effect is not disclosed at the point of execution with a prompt, warning, or user-facing log. Because this modifies local user data storage during a status-check operation, it should be made explicit to the user.

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

python
# Otherwise: spawn daemon subprocess, wait for it to bind, print JSON success, exit so the agent gets immediate response.
    env = {**os.environ, "COMFYUI_SERVE_DAEMON": "1"}
    child = Popen(
        [sys.executable, __file__],
        cwd=str(SKILL_ROOT),
        env=env,

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
86% confidence
Finding

This markdown file states that all images are fetched and saved into generated/, which affects user data storage on disk. The description does not include any warning or disclosure about local persistence, overwrite/retention expectations, or handling of potentially sensitive generated content.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
76% confidence
Finding

The delete_profile function removes stored profile data and rewrites the persistent user config file, which is a potentially destructive action. The code contains no confirmation prompt, log/print statement, or warning comment indicating that saved configuration will be deleted.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
74% confidence
Finding

The clear_queue and interrupt functions issue POST requests that can cancel or remove in-progress work on the ComfyUI server. Although these actions fit the API purpose, the code provides no confirmation, logging, or warning comment to indicate that remote queued or running jobs may be affected.

Content

No source excerpt is available for this finding.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · scripts/run.py (reported line 61)May include surrounding context.

python
"password": "COMFYUI_PASSWORD",
    }
    for attr, env_name in mapping.items():
        value = getattr(args, attr, None)
        if value not in (None, ""):
            os.environ[env_name] = str(value)

Static analysis

No suspicious patterns detected.