Back to skill

Security audit

image

Security checks for vulnerabilities and agentic risk

Overview

This ComfyUI skill has a real image-workflow purpose, but it also gives the agent broad download and local-write authority, including automatic installation and execution of an unverified external downloader.

Review before installing. Use this only if you are comfortable letting an agent modify workflows, write into ~/ComfyUI, contact arbitrary model URLs, and possibly install pget into ~/.local/bin. Prefer running downloads with --no-pget, only use trusted HTTPS model sources, avoid --overwrite, and inspect or remove bundled workflow assets with unexpected sensitive prompts.

Vulnerability Patterns
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/download_weights.py:96
Finding
Encoded Path Traversal Allows Arbitrary File Writes## Vulnerability Details **File Location**: `scripts/download_weights.py`, lines 96–121 and 137–149 **Vulnerability Type**: Path traversal and unrestricted file write **Risk Level**: High ### Vulnerable Code ```python def resolve_url_dest( raw: str, base: str, overwrite: bool, default_subfolder: str | None ) -> tuple[str | None, str | None, bool]: """Return (url, dest_path, is_skip). url None = skip/ignore.""" url = raw.strip() if not url or url.startswith("#"): return (None, None, True) subfolder = default_subfolder if default_subfolder in SUBFOLDERS else "checkpoints" if " " in url: url, sub = url.strip().split(None, 1) sub = sub.strip().lower() if sub in SUBFOLDERS: subfolder = sub elif not default_subfolder or default_subfolder not in SUBFOLDERS: subfolder = infer_subfolder(url) base = os.path.expanduser(base) model_dir = os.path.join(base, "models", subfolder) os.makedirs(model_dir, exist_ok=True) path = urlparse(url).path name = path.rstrip("/").split("/")[-1] name = unquote(name) if name else "downloaded.safetensors" out_path = os.path.join(model_dir, name) if os.path.isfile(out_path) and not overwrite: return (None, out_path, True) return (url, out_path, False) ``` ```python def download_one_fallback( url: str, dest_path: str, overwrite: bool ) -> tuple[str, str]: """Download one file with urllib. Returns (status, path_or_message).""" if os.path.isfile(dest_path) and not overwrite: return ("skipped", dest_path) req = urllib.request.Request(url, headers={"User-Agent": "ComfyUI-Skill/1.0"}) try: with urllib.request.urlopen(req, timeout=600) as resp: with open(dest_path, "wb") as f: while True: chunk = resp.read(1 << 20) if not chunk: ...[truncated 2343 chars]
Remediation
## Remediation Suggestions - Decode the URL path before extracting the final filename, then reduce it to a single safe basename. - Reject filenames containing `/`, `\`, NUL characters, `.` or `..` path components, control characters, or platform-specific separators. - Resolve the model directory and proposed destination with `pathlib.Path.resolve()`. - Verify confinement with `destination.is_relative_to(model_directory)` on supported Python versions, or use a reliable `os.path.commonpath()` comparison. - Generate a safe local filename when the remote filename is missing or invalid. - Create downloads using exclusive file creation where practical and require explicit confirmation before replacing existing files. - Write to a secure temporary file inside the validated destination directory, verify the result, and atomically rename it into place. - Apply the same validated path to both the built-in downloader and the `pget` manifest. - Add regression tests covering encoded `/` and `\` separators, encoded `..`, absolute paths, mixed encoding, and platform-specific traversal forms.

T03 · Remote Payload Retrieval and Execution

Warning
Location
scripts/download_weights.py:18
Finding
Unpinned Remote Binary Is Automatically Downloaded and Executed## Vulnerability Details **File Location**: `scripts/download_weights.py`, lines 18 and 60–90, with execution at lines 126–130 **Vulnerability Type**: Unverified remote payload retrieval and execution **Risk Level**: Medium ### Vulnerable Code ```python PGET_RELEASE = "https://github.com/replicate/pget/releases/latest/download" ``` ```python def get_pget_binary() -> str | None: """Return path to pget binary, or None if not found and install failed.""" pget = shutil.which("pget") if pget: return pget # Install to ~/.local/bin local_bin = os.path.expanduser("~/.local/bin") pget_path = os.path.join(local_bin, "pget") if os.path.isfile(pget_path) and os.access(pget_path, os.X_OK): return pget_path os.makedirs(local_bin, exist_ok=True) sysname = platform.system() machine = platform.machine() if machine == "aarch64": machine = "arm64" elif machine == "x86_64": machine = "x86_64" # Replicate releases: pget_Linux_x86_64, pget_Darwin_arm64, etc. asset = f"pget_{sysname}_{machine}" url = f"{PGET_RELEASE}/{asset}" try: req = urllib.request.Request(url, headers={"User-Agent": "ComfyUI-Skill/1.0"}) with urllib.request.urlopen(req, timeout=60) as resp: with open(pget_path, "wb") as f: f.write(resp.read()) os.chmod(pget_path, 0o755) return pget_path except Exception as e: print(f"Could not install pget ({e}); falling back to built-in download.", file=sys.stderr) return None ``` ```python def download_with_pget(manifest_path: str, pget_bin: str, overwrite: bool) -> bool: cmd = [pget_bin, "multifile", manifest_path] if overwrite: cmd.append("-f") r = subprocess.run(cmd) return r.returncode == 0 ``` ### Technical Analysis When `pget` is not already available, the script downloads a ...[truncated 1918 chars]
Remediation
## Remediation Suggestions - Do not automatically download and execute a binary as an implicit part of a model download. - Prefer the built-in downloader by default and require explicit user approval before installing an external executable. - Pin `pget` to a specific audited version rather than using a mutable `latest` URL. - Maintain platform-specific SHA-256 or stronger cryptographic digests in the reviewed source code. - Download to a secure temporary file, verify its digest and any available publisher signature, and only then install and execute it. - Validate the final redirect destination and reject unexpected hosts or insecure redirects. - Use a package manager or trusted dependency mechanism that provides integrity and provenance verification where available. - Avoid installing into a general executable directory automatically. Use a skill-specific cache directory with restrictive permissions. - Document the exact external dependency version and provide a reproducible update process for digest changes.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/download_weights.py:96
Finding
Unrestricted Download URLs Permit Local File Access and Server-Side Request Forgery## Vulnerability Details **File Location**: `scripts/download_weights.py`, lines 96–121 and 137–149 **Vulnerability Type**: Unrestricted URL scheme and destination access **Risk Level**: Medium ### Vulnerable Code ```python def resolve_url_dest( raw: str, base: str, overwrite: bool, default_subfolder: str | None ) -> tuple[str | None, str | None, bool]: """Return (url, dest_path, is_skip). url None = skip/ignore.""" url = raw.strip() if not url or url.startswith("#"): return (None, None, True) subfolder = default_subfolder if default_subfolder in SUBFOLDERS else "checkpoints" if " " in url: url, sub = url.strip().split(None, 1) sub = sub.strip().lower() if sub in SUBFOLDERS: subfolder = sub elif not default_subfolder or default_subfolder not in SUBFOLDERS: subfolder = infer_subfolder(url) base = os.path.expanduser(base) model_dir = os.path.join(base, "models", subfolder) os.makedirs(model_dir, exist_ok=True) path = urlparse(url).path name = path.rstrip("/").split("/")[-1] name = unquote(name) if name else "downloaded.safetensors" out_path = os.path.join(model_dir, name) if os.path.isfile(out_path) and not overwrite: return (None, out_path, True) return (url, out_path, False) ``` ```python def download_one_fallback( url: str, dest_path: str, overwrite: bool ) -> tuple[str, str]: """Download one file with urllib. Returns (status, path_or_message).""" if os.path.isfile(dest_path) and not overwrite: return ("skipped", dest_path) req = urllib.request.Request(url, headers={"User-Agent": "ComfyUI-Skill/1.0"}) try: with urllib.request.urlopen(req, timeout=600) as resp: with open(dest_path, "wb") as f: while True: chunk = resp.read(1 << 20) if not ch ...[truncated 2548 chars]
Remediation
## Remediation Suggestions - Permit only `https` URLs by default and reject `file`, `ftp`, `data`, and other schemes. - Reject embedded credentials and unexpected ports unless the user explicitly authorizes them. - Resolve hostnames before connecting and block loopback, private, link-local, multicast, reserved, and unspecified IP ranges. - Account for IPv4, IPv6, alternative textual representations, and DNS rebinding. - Disable redirects or validate the scheme, hostname, port, and resolved address again after every redirect. - Optionally maintain an allowlist of trusted model-hosting domains. - Separate an explicit local-file import feature from remote downloading if local imports are legitimately required. - Apply equivalent restrictions when using `pget`; otherwise, use only the hardened built-in client. - Add tests for `file://`, localhost, private IPv4 ranges, IPv6 loopback and link-local addresses, redirects to blocked destinations, and hostname resolution changes.
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (13)

Self-Modification

High
Category
Rogue Agent
Content
## Updating the Skill

- When ComfyUI's API or workflow format changes, update SKILL.md and the run script.
- Keep the "If the server isn't reachable" section in sync with current ComfyUI install/run instructions.
- Bump version when publishing a new release to SkillBoss Hub.
Confidence
85% confidence
Finding
Skill modifies its own code, configuration, or behavior at runtime. Self-modification enables an agent to escalate privileges, disable safety constraints, or install persistent backdoors.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The declared purpose is to run local ComfyUI workflows, but the skill also downloads arbitrary remote files, installs an external binary, and writes model files into the local ComfyUI tree. That mismatch can cause an agent or reviewer to underappreciate the real risk surface, enabling supply-chain exposure and unexpected system modification under a benign-sounding skill description.

Description-Behavior Mismatch

High
Confidence
95% confidence
Finding
This file adds capability to fetch arbitrary URLs and write files into the local ComfyUI models tree, which materially expands the skill beyond its declared scope of invoking local ComfyUI workflows over HTTP. Scope mismatch is dangerous because it enables unreviewed network access and host filesystem modification, increasing the chance of supply-chain abuse, unauthorized model placement, or operational policy bypass.

Context-Inappropriate Capability

High
Confidence
98% confidence
Finding
The script downloads an executable from a remote GitHub release URL, writes it to ~/.local/bin, marks it executable, and then runs it. That creates an unnecessary remote code execution and persistence path for a skill whose stated purpose is only to run local ComfyUI workflows via HTTP API, so any compromise of the download source, network path, or release artifact could execute attacker-controlled code on the host.

Self-Modification

High
Category
Rogue Agent
Content
ap.add_argument(
        "--overwrite",
        action="store_true",
        help="Overwrite existing files",
    )
    ap.add_argument(
        "--no-pget",
Confidence
85% confidence
Finding
Skill modifies its own code, configuration, or behavior at runtime. Self-modification enables an agent to escalate privileges, disable safety constraints, or install persistent backdoors.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill instructs reading files, invoking shell commands, and using network access, but it declares no explicit tool scope or permission boundaries. That increases the chance an agent can perform filesystem and network actions without clear policy constraints, especially because the skill also handles user-supplied workflow paths and URLs.

Vague Triggers

Medium
Confidence
87% confidence
Finding
The activation conditions include generic image-generation prompts, so the skill may trigger on ordinary requests that do not explicitly ask to use local ComfyUI or modify the host environment. In this skill, over-broad activation is more dangerous because triggering it can lead to local file access, shell execution, service startup, or model downloads.

Session Persistence

Medium
Category
Rogue Agent
Content
2. **Identify prompt-related nodes** by inspecting the graph: look for nodes that hold the main text prompt — e.g. `PrimitiveStringMultiline`, `CLIPTextEncode` (positive text), or any node with `_meta.title` or `class_type` suggesting "Prompt" / "positive" / "text". Update the corresponding input (e.g. `inputs.value`, or the text input to the encoder) to the image prompt you derived from the user (subject, style, lighting, quality). If the user didn't ask for a custom image, you can leave the existing prompt or tweak only if needed.
3. **Optionally identify style/prefix nodes** — e.g. `StringConcatenate`, or a second string input that acts as style. Set them if the user asked for a specific style or to clear a default prefix.
4. **Optionally set a new seed** — find sampler-like nodes (e.g. `KSampler`, `BasicGuider`, or any node with a `seed` input) and set `seed` to a new random integer so each run can differ.
5. Write the modified workflow to a temp file (e.g. `skills/comfyui/assets/tmp-workflow.json`). Use `~/ComfyUI/venv/bin/python` for any inline Python; do not use bare `python`.
6. Run: `comfyui_run.py --workflow <path-to-edited-json>`.

If the workflow structure is unclear or you can't find prompt/sampler nodes, run the file as-is and only change what you can reliably identify. Same approach for arbitrary user-supplied JSON: inspect first, edit at your best knowledge, then run.
Confidence
76% confidence
Finding
The skill persists a modified workflow to a predictable temp-like path inside the skill assets directory, which can leave prompt content or user-supplied workflow modifications on disk across sessions. While not as severe as code execution, this can leak sensitive prompts or cause later runs to reuse stale state unexpectedly.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The skill tells the agent to download model weights from arbitrary URLs and to install pget automatically if missing, but it does not require an explicit warning or confirmation before making persistent filesystem changes or introducing new executables. This creates clear supply-chain and local-environment risks, especially when the URLs may come directly from the user.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The default workflow hard-codes an ethnicity-specific prompt ('Latina female') without any user choice, context, or justification. In an image-generation skill, this can bias outputs, steer users toward demographic-specific depictions they did not request, and create fairness and representation concerns even though it is not a code-execution or system-compromise issue.

Description-Behavior Mismatch

Medium
Confidence
98% confidence
Finding
The bundled default workflow contains an explicit nude-generation prompt ('Adult woman, nude...') that is not disclosed in the skill description. This is dangerous because invoking the default asset could unexpectedly generate sexual content, creating safety, compliance, and trust issues for downstream users or systems that assume a generic image workflow.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
cmd = [pget_bin, "multifile", manifest_path]
    if overwrite:
        cmd.append("-f")
    r = subprocess.run(cmd)
    return r.returncode == 0
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Low
Confidence
80% confidence
Finding
This markdown file states that the agent will edit workflow JSON and write a temp workflow file, which affects the user's local filesystem. The description does not include any caution or user-facing warning about where the file is written or that local files will be modified.

Static analysis

No suspicious patterns detected.