Back to skill

Security audit

Image Gen Compare

Security checks for vulnerabilities and agentic risk

Overview

This image-comparison skill mostly does what it says, but it can silently save generated images to a Proton Drive-synced folder and uses a 1Password service-token fallback that is broader than the user-facing instructions make clear.

Review before installing. Use it only with prompts and generated images you are comfortable sending to OpenAI and potentially storing in a synced Proton Drive folder. Prefer setting OPENAI_API_KEY directly, avoid running it in an environment full of unrelated secrets, and inspect or change the output directory before using confidential creative work.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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
Findings (3)

other

Warning
Location
scripts/image_gen_compare.py:40
Finding
Generated Images Are Silently Written to Cloud-Synchronized Storage<![CDATA[ ## Vulnerability Details **File Location**: `scripts/image_gen_compare.py:40-43`, with generated image writes at `scripts/image_gen_compare.py:116-119`, `scripts/image_gen_compare.py:176-179`, and `scripts/image_gen_compare.py:241-244` **Vulnerability Type**: Unintended cloud data exposure **Risk Level**: Medium ### Complete Code Snippet ```python # Save to Proton Drive Artifacts (synced, visible in Proton Drive app) _PROTON = Path.home() / "Library/CloudStorage/ProtonDrive-user@proton.me-folder/Artifacts/images" _today = datetime.now().strftime("%Y/%m/%d") OUTPUT_DIR = _PROTON / _today if _PROTON.parent.exists() else WORKSPACE / "content" / "images" OUTPUT_DIR.mkdir(parents=True, exist_ok=True) ``` The selected directory is subsequently used for generated output: ```python filename = f"dalle3_{quality}_{ts}.png" dest = OUTPUT_DIR / filename img_resp = requests.get(image_url, timeout=30) dest.write_bytes(img_resp.content) ``` Equivalent writes occur for FLUX and SDXL output: ```python dest = OUTPUT_DIR / filename image.image.save(str(dest)) ``` ```python dest = OUTPUT_DIR / filename result_img.save(str(dest)) ``` ### Technical Analysis The script automatically checks for a hard-coded Proton Drive directory and selects it whenever its parent exists. Files written into this directory are expected to be synchronized by the installed Proton Drive client. This behavior is not clearly disclosed by the Skill documentation. `SKILL.md` describes output as being saved under the workspace and declares outbound networking for calls to the OpenAI API. It does not state that generated images may be placed in third-party cloud-synchronized storage. Cloud storage is not necessary for the declared image comparison functionality. Automatically selecting it therefore exceeds the minimum storage and data-transfer privileges required by the Skill. The hard-coded account-specific path also makes the behavior non-portable and may direct data to an account that th ...[truncated 1215 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Default all output to a project-local directory: ```python OUTPUT_DIR = WORKSPACE / "content" / "images" ``` 2. Add an explicit `--output-dir` option for users who want another location. 3. Require an explicit `--cloud-sync` option or confirmation before selecting any synchronized directory. 4. Clearly document when output may leave the local workspace and identify the relevant synchronization provider. 5. Avoid hard-coded usernames or account-specific cloud paths. 6. Resolve and display the selected output path before generation, and require confirmation when it is under a known synchronized directory. 7. Consider warning users when prompts or generated images may contain sensitive information. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/image_gen_compare.py:61
Finding
Security-Sensitive Credential Helper Inherits the Entire Process Environment<![CDATA[ ## Vulnerability Details **File Location**: `scripts/image_gen_compare.py:61-74` **Vulnerability Type**: Excessive credential exposure to a PATH-resolved subprocess **Risk Level**: Medium ### Complete Code Snippet ```python def _get_openai_key() -> str: key = os.environ.get("OPENAI_API_KEY") if key: return key try: token_path = Path.home() / ".config/openclaw/.op-service-token" env = os.environ.copy() env["OP_SERVICE_ACCOUNT_TOKEN"] = token_path.read_text().strip() result = subprocess.run( ["op", "read", "op://OpenClaw/OpenAI API Key/credential"], capture_output=True, text=True, env=env, timeout=10, ) if result.returncode == 0: return result.stdout.strip() except Exception as exc: print(f"Warning: 1Password fetch failed: {exc}", file=sys.stderr) raise RuntimeError("No OPENAI_API_KEY found") ``` ### Technical Analysis When `OPENAI_API_KEY` is absent, the script reads a persistent 1Password service-account token from the user's home directory. It then copies the entire parent environment, adds the service token, and invokes `op` by a bare executable name. Two security weaknesses are combined: - The child receives every environment variable inherited by the script, including unrelated credentials that may be present. - The executable is resolved through `PATH`, so the code does not verify that the invoked program is the intended 1Password CLI binary. A legitimate 1Password CLI needs the service token for this operation, but it does not need unrelated environment secrets. Passing the complete environment violates least-privilege principles for a subprocess handling security-sensitive credentials. The script itself does not transmit the token to OpenAI, print it, or otherwise directly exfiltrate it. Exploitation requires control over executable resolution or compromise of the invoked `op` program. ### Attack Path 1. `OPENAI_API_K ...[truncated 1217 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Prefer requiring `OPENAI_API_KEY` directly unless credential-manager integration is explicitly enabled. 2. Locate the helper using a trusted absolute path or verify the resolved path: ```python op_path = shutil.which("op") ``` Confirm that the result belongs to an expected installation location and is not writable by untrusted users. 3. Pass a minimal allowlisted environment instead of `os.environ.copy()`: ```python env = { "PATH": "/usr/local/bin:/usr/bin:/bin", "HOME": str(Path.home()), "OP_SERVICE_ACCOUNT_TOKEN": token, } ``` 4. Validate that the token file is a regular file owned by the current user and has restrictive permissions. 5. Limit the 1Password service account to the single required vault item. 6. Avoid retaining the token longer than necessary and remove references after subprocess completion. 7. Document the credential fallback and provide an option to disable it. ]]>

T08 · Insecure Dependencies

Warning
Location
scripts/image_gen_compare.py:145
Finding
Unpinned Python Dependencies and Mutable Runtime Model Downloads<![CDATA[ ## Vulnerability Details **File Location**: `scripts/image_gen_compare.py:145-164` and `scripts/image_gen_compare.py:206-223` **Vulnerability Type**: Unpinned third-party dependencies and model artifacts **Risk Level**: Medium ### Complete Code Snippet ```python def generate_flux(prompt: str, steps: int = 4, seed: int | None = None) -> dict: """Generate an image with FLUX.1-schnell via mflux (local, free).""" try: # mflux v0.16+ uses explicit module paths from mflux.models.flux.variants.txt2img.flux import Flux1 from mflux.models.common.config.model_config import ModelConfig except ImportError: raise RuntimeError("mflux not installed. Run: pip install mflux") print(f"\n🖥️ FLUX.1-schnell (local, {LINKEDIN_W}×{LINKEDIN_H}, {steps} steps, free)...") print(" (First run: downloads ~9GB model from HuggingFace — grab a coffee)") start = time.monotonic() # Load model (downloads from HuggingFace on first run) flux = Flux1( model_config=ModelConfig.schnell(), quantize=8, ) ``` ```python def generate_sdxl(prompt: str, steps: int = 30, seed: int | None = None) -> dict: """Generate with SDXL base via diffusers on Apple MPS. Free, ungated, ~7GB model.""" try: import torch from diffusers import StableDiffusionXLPipeline except ImportError: raise RuntimeError("diffusers not installed. Run: pip install diffusers accelerate") if not torch.backends.mps.is_available(): raise RuntimeError("MPS (Apple Metal) not available — is this Apple Silicon?") print(f"\n🍎 SDXL base (MPS, {SDXL_W}×{SDXL_H}, {steps} steps, free)...") print(" (First run: downloads ~7GB model from HuggingFace)") start = time.monotonic() pipe = StableDiffusionXLPipeline.from_pretrained( "stabilityai/stable-diffusion-xl-base-1.0", torch_dtype=torch.float32, use_safetensors=True, ) ``` ### Technical Analysis The error ...[truncated 2353 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Add a locked dependency manifest with exact versions and cryptographic hashes. 2. Replace generic installation instructions with installation from the reviewed lockfile: ```bash pip install --require-hashes -r requirements.lock ``` 3. Pin all transitive dependencies using a reproducible dependency-management tool. 4. Pin Hugging Face model repositories to reviewed immutable commit revisions. 5. Verify downloaded artifact hashes before loading them. 6. Disable remote custom code unless explicitly reviewed and required. 7. Use an isolated virtual environment with minimal filesystem and network privileges. 8. Document model download hosts, expected sizes, revisions, and storage locations. 9. Periodically review pinned versions for published vulnerabilities before updating them. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • 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
Findings (15)

Tainted flow: 'key' from os.environ.get (line 61, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Content
print(f"\n🎨 DALL-E 3 ({quality}, {DALLE_W}×{DALLE_H}, ~${cost:.2f})...")
    start = time.monotonic()

    resp = requests.post(
        "https://api.openai.com/v1/images/generations",
        headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
        json={
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The documented behavior does not fully match the skill's effective capabilities: it can access a 1Password service-account token to retrieve secrets and may write outputs into a synced storage location, neither of which are clearly disclosed in the primary description. This creates a trust and review gap where users may authorize a seemingly simple image-comparison skill without realizing it can handle secrets and persist generated content to broader storage destinations.

Context-Inappropriate Capability

High
Confidence
97% confidence
Finding
The code intentionally writes generated images to a Proton Drive-synced directory when present, causing automatic replication of user prompts' outputs outside the local workspace. In the context of a model-comparison utility, this is an unnecessary externalization channel that can leak sensitive or proprietary generated content.

Env Variable Harvesting

High
Category
Data Exfiltration
Content
return key
    try:
        token_path = Path.home() / ".config/openclaw/.op-service-token"
        env = os.environ.copy()
        env["OP_SERVICE_ACCOUNT_TOKEN"] = token_path.read_text().strip()
        result = subprocess.run(
            ["op", "read", "op://OpenClaw/OpenAI API Key/credential"],
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.

Lp3

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding
The skill declares significant capabilities in metadata and usage patterns—environment variable access, network access, shell execution, and file read/write—but does not define an explicit tool scope such as permissions or allowed-tools. This weakens the security boundary by making it harder for reviewers and enforcement systems to verify that the skill only uses the minimum necessary privileges.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The script documentation says outputs are saved under the workspace, but the code may instead write generated images to a Proton Drive-synced path outside the workspace. This hidden data egress/storage behavior increases the risk of unintentionally exposing prompts and generated artifacts to synced cloud storage.

Context-Inappropriate Capability

Medium
Confidence
84% confidence
Finding
The skill reaches beyond simple image generation by reading a local service-account token file and invoking the 1Password CLI to retrieve a secret. That secret access pattern expands the skill's privilege footprint and is not obvious from the high-level description, increasing the risk of unintended credential use in automated environments.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
token_path = Path.home() / ".config/openclaw/.op-service-token"
        env = os.environ.copy()
        env["OP_SERVICE_ACCOUNT_TOKEN"] = token_path.read_text().strip()
        result = subprocess.run(
            ["op", "read", "op://OpenClaw/OpenAI API Key/credential"],
            capture_output=True, text=True, env=env, timeout=10,
        )
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

External Transmission

Medium
Category
Data Exfiltration
Content
print(f"\n🎨 DALL-E 3 ({quality}, {DALLE_W}×{DALLE_H}, ~${cost:.2f})...")
    start = time.monotonic()

    resp = requests.post(
        "https://api.openai.com/v1/images/generations",
        headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
        json={
Confidence
70% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
print(f"\n🎨 DALL-E 3 ({quality}, {DALLE_W}×{DALLE_H}, ~${cost:.2f})...")
    start = time.monotonic()

    resp = requests.post(
        "https://api.openai.com/v1/images/generations",
        headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
        json={
Confidence
80% 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
95% confidence
Finding
User-supplied prompt text is sent to OpenAI, but the script does not provide an explicit runtime warning or consent mechanism about external data sharing. In a comparison tool that may be used with proprietary creative briefs or internal prompts, this omission increases the risk of accidental disclosure.

External Transmission

Medium
Category
Data Exfiltration
Content
start = time.monotonic()

    resp = requests.post(
        "https://api.openai.com/v1/images/generations",
        headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
        json={
            "model": "dall-e-3",
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

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

Medium
Category
Data Flow
Content
filename = f"dalle3_{quality}_{ts}.png"
    dest = OUTPUT_DIR / filename

    img_resp = requests.get(image_url, timeout=30)
    dest.write_bytes(img_resp.content)

    result = {
Confidence
87% confidence
Finding
The code performs a second network fetch against a URL returned by a remote service without validating the hostname, scheme, or content type. If the upstream response were compromised or unexpectedly changed, this could enable unintended outbound requests (SSRF-like behavior) or download of untrusted content into local/cloud-synced storage.

Description-Behavior Mismatch

Medium
Confidence
93% confidence
Finding
The manifest description frames the skill around comparing specific paid and local models, explicitly naming DALL-E 3, FLUX.1-schnell, Gemini Imagen, and others. In the code, an additional SDXL generator is implemented, while no Gemini Imagen integration exists anywhere in the file, so the actual supported model set diverges from the stated description.

Missing User Warnings

Low
Confidence
90% confidence
Finding
The skill description says it logs metadata and stores run history, but does not clearly warn users that generated images and associated metadata will be retained. In this context, prompts and outputs may contain sensitive or proprietary material, so undisclosed persistence increases privacy and data-handling risk.

Static analysis

No suspicious patterns detected.