Back to skill

Security audit

Filtrix Image Generation

Security checks for vulnerabilities and agentic risk

Overview

This image generation and editing skill mostly matches its stated purpose, but it needs review because it disables a fal.ai safety check and downloads provider-returned URLs without validation.

Review before installing. Use this only with API keys you intend to spend from, avoid sending private images or sensitive prompts unless you trust the selected provider, and be aware that fal.ai editing disables a provider safety filter by default. The URL download handling should be hardened before use in sensitive or network-restricted environments.

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/edit.py:124
Finding
Unvalidated Provider-Controlled URL Retrieval<![CDATA[ ## Vulnerability Details **File Location**: `scripts/edit.py:124-129`, `scripts/edit.py:255-264`, `scripts/generate.py:88-93`, and `scripts/generate.py:207-213` **Vulnerability Type**: Server-Side Request Forgery and Unrestricted Resource Retrieval **Risk Level**: Medium ### Vulnerable Code From `scripts/edit.py:124-129`: ```python b64 = result.get("data", [{}])[0].get("b64_json") if not b64: url = result.get("data", [{}])[0].get("url") if url: with urllib.request.urlopen(url) as img_resp: return img_resp.read() ``` From `scripts/edit.py:255-264`: ```python img_url = images[0].get("url", "") # Handle data URI (sync_mode) or HTTP URL if img_url.startswith("data:"): b64_data = img_url.split(",", 1)[1] return base64.b64decode(b64_data) else: with urllib.request.urlopen(img_url) as img_resp: return img_resp.read() ``` From `scripts/generate.py:88-93`: ```python if not b64: # Try URL fallback url = body.get("data", [{}])[0].get("url") if url: with urllib.request.urlopen(url) as img_resp: return img_resp.read() ``` From `scripts/generate.py:207-213`: ```python img_url = images[0].get("url") if not img_url: raise RuntimeError("No image URL in fal response") with urllib.request.urlopen(img_url) as img_resp: return img_resp.read() ``` ### Technical Analysis The scripts extract image URLs from OpenAI or fal.ai responses and pass them directly to `urllib.request.urlopen`. They do not validate: - The URL scheme, such as restricting retrieval to HTTPS. - The destination hostname against an approved provider CDN allowlist. - Whether DNS resolution points to loopback, private, link-local, multicast, or reserved addresses. - Redirect destinations. - The response content type or maximum response size. - Download timeouts for these image retrieval requests. This create ...[truncated 1947 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse every returned URL with `urllib.parse.urlsplit` and permit only the `https` scheme. 2. Maintain an allowlist of documented provider image-delivery domains. Do not permit arbitrary hosts solely because the initial API host is trusted. 3. Resolve the destination hostname before connecting and reject all loopback, private, link-local, multicast, unspecified, and reserved IP addresses using Python's `ipaddress` module. 4. Apply the same validation to every redirect destination, or disable automatic redirects and process them manually. 5. Add explicit connection and read timeouts to all image downloads. 6. Stream responses in bounded chunks and reject downloads above a configured maximum size. 7. Verify that the response `Content-Type` is an expected image media type and validate the file signature before writing it. 8. For fal data URIs, require the expected `data:image/...;base64,` structure and enforce a decoded-size limit. 9. Prefer inline image data from authenticated API responses when the provider supports it, avoiding secondary URL retrieval where practical. ]]>

other

Note
Location
scripts/edit.py:221
Finding
fal.ai Image Editing Explicitly Disables the Provider Safety Checker<![CDATA[ ## Vulnerability Details **File Location**: `scripts/edit.py:221-229` **Vulnerability Type**: Content Safety Control Bypass **Risk Level**: Low ### Vulnerable Code ```python arguments = { "prompt": prompt, "image_size": {"width": dims["width"], "height": dims["height"]}, "num_images": 1, "max_images": 1, "enable_safety_checker": False, "enhance_prompt_mode": "standard", "sync_mode": True, "image_urls": [data_uri], } ``` ### Technical Analysis The fal.ai editing request explicitly sets `enable_safety_checker` to `False`. Ordinary image editing does not require disabling this provider-side safeguard, and neither `README.md` nor `SKILL.md` discloses that the fal editing path turns it off. This configuration weakens a security and abuse-prevention control by default. It therefore exceeds the minimum privileges and behavior required for the Skill's declared image-editing functionality. The issue is limited to the fal.ai editing path. The reviewed OpenAI and Gemini paths do not contain an equivalent explicit safety-filter disablement. ### Attack Path 1. A user selects fal.ai as the image-editing provider. 2. The user supplies an input image and editing prompt. 3. The script submits both to fal.ai with `enable_safety_checker` set to `False`. 4. Provider-side content that would otherwise be rejected by that checker may be generated and returned. 5. The script writes the returned content to the selected output path without warning that the safety control was disabled. ### Impact Assessment The primary impact is an increased likelihood of producing harmful, disallowed, or policy-violating visual content. It may also violate operator expectations, organizational content policies, or provider usage requirements. This issue does not provide system-level privileges, filesystem access beyond the script's normal output behavior, or arbitrary code execution. Its scope is the weakening ...[truncated 68 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove the `enable_safety_checker` field so the provider's secure default applies, or explicitly set it to `True`. 2. Document the active content-safety behavior in `README.md` and `SKILL.md`. 3. If disabling the checker is legitimately required in a permitted deployment, expose it only as an explicit, documented, operator-controlled option rather than the default. 4. Require clear confirmation before allowing any opt-out and ensure that it complies with provider terms and organizational policy. 5. Add tests verifying that ordinary fal.ai editing requests do not disable provider safety controls. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (21)

Tainted flow: 'req' from os.environ.get (line 237, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
)

    try:
        with urllib.request.urlopen(req) as resp:
            result = json.loads(resp.read())
    except urllib.error.HTTPError as e:
        err_body = e.read().decode()
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'req' from os.environ.get (line 237, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
)

    try:
        with urllib.request.urlopen(req) as resp:
            result = json.loads(resp.read())
    except urllib.error.HTTPError as e:
        err_body = e.read().decode()
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'url' from os.environ.get (line 175, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
if not b64:
        url = result.get("data", [{}])[0].get("url")
        if url:
            with urllib.request.urlopen(url) as img_resp:
                return img_resp.read()
        raise RuntimeError("No image data in OpenAI edit response")
Confidence
90% confidence
Finding
The code follows a URL returned by the OpenAI API and fetches it without validating the scheme, host, or IP range. If the upstream response is compromised or altered, this creates an SSRF-style outbound fetch primitive that could access internal services or unexpected hosts from the runtime environment.

Tainted flow: 'req' from os.environ.get (line 237, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
)

    try:
        with urllib.request.urlopen(req, timeout=180) as resp:
            body = json.loads(resp.read())
    except urllib.error.HTTPError as e:
        err_body = e.read().decode()
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'req' from os.environ.get (line 229, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
)

    try:
        with urllib.request.urlopen(req) as resp:
            body = json.loads(resp.read())
    except urllib.error.HTTPError as e:
        err_body = e.read().decode()
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'req' from os.environ.get (line 229, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
)

    try:
        with urllib.request.urlopen(req) as resp:
            body = json.loads(resp.read())
    except urllib.error.HTTPError as e:
        err_body = e.read().decode()
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'req' from os.environ.get (line 229, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
)

    try:
        with urllib.request.urlopen(req) as resp:
            body = json.loads(resp.read())
    except urllib.error.HTTPError as e:
        err_body = e.read().decode()
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'req' from os.environ.get (line 229, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
)

    try:
        with urllib.request.urlopen(req) as resp:
            body = json.loads(resp.read())
    except urllib.error.HTTPError as e:
        err_body = e.read().decode()
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'url' from os.environ.get (line 125, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
# Try URL fallback
        url = body.get("data", [{}])[0].get("url")
        if url:
            with urllib.request.urlopen(url) as img_resp:
                return img_resp.read()
        raise RuntimeError("No image data in OpenAI response")
Confidence
90% confidence
Finding
The code blindly fetches an image from a URL returned by the OpenAI API without validating the scheme, host, or content type. If that upstream response were compromised or unexpected, the skill could be induced to make arbitrary outbound requests, creating an SSRF-style primitive and downloading untrusted content to disk.

Tainted flow: 'req' from os.environ.get (line 229, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
)

    try:
        with urllib.request.urlopen(req, timeout=180) as resp:
            body = json.loads(resp.read())
    except urllib.error.HTTPError as e:
        err_body = e.read().decode()
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
98% confidence
Finding
The declared purpose emphasizes image generation, but the skill also supports image editing, masking, and processing of local input image files with output written to disk. That mismatch matters because users and policy systems may authorize a generation-only skill while overlooking that it can ingest local files and transform existing content, which broadens the data-handling and privacy exposure.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The declared purpose emphasizes image generation, but the skill also supports image editing, masking, and processing of local input image files with output written to disk. That mismatch matters because users and policy systems may authorize a generation-only skill while overlooking that it can ingest local files and transform existing content, which broadens the data-handling and privacy exposure.

External Model or Provider Selection

High
Category
Excessive Agency
Content
if not api_key:
        raise RuntimeError("GOOGLE_API_KEY not set")

    # Default to Flash (cheaper). Use --model gemini-3-pro-image-preview for higher quality.
    model = model or "gemini-2.5-flash-image"
    aspect = GEMINI_ASPECT_MAP.get(size, "1:1")
Confidence
90% confidence
Finding
Skill selects an external model or provider that may use a different account or billing plan than the operator expects. Undisclosed model switches can cause unexpected cost or quota consumption.

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill documentation advertises behavior that uses environment variables, local file reads/writes, and network access, but it does not declare any explicit tool scope or permissions. This creates a least-privilege and transparency problem: an agent or reviewer cannot easily constrain the skill to only the capabilities it genuinely needs, increasing the chance of unintended credential access, filesystem interaction, or outbound requests.

Vague Triggers

Medium
Confidence
93% confidence
Finding
The trigger description is very broad and can capture many ordinary image-related requests, causing the skill to activate in situations where users may not expect external API calls, file creation, or provider selection based on available keys. Over-broad invocation increases the chance of unnecessary data disclosure to third-party providers and accidental use of local or environmental resources.

Description-Behavior Mismatch

Medium
Confidence
96% confidence
Finding
The manifest says the skill is for generating images when a user asks to create or make an image, but this file is explicitly an image editor and requires an existing input image to transform. The docstring, provider functions, and CLI all implement image-to-image editing, which is a materially different user-facing behavior from pure image generation.

Context-Inappropriate Capability

Medium
Confidence
91% confidence
Finding
If an arbitrary model string is supplied, the code uses it directly to construct the fal.run path instead of restricting requests to the declared supported models. This expands the reachable external capability surface and can bypass intended policy controls, cost controls, or safety expectations tied to the documented provider/model scope.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The fal provider explicitly disables the safety checker, which increases the chance of generating disallowed, harmful, or policy-violating content without any user warning. In a skill exposed to end users, disabling provider safeguards materially raises misuse risk and can undermine platform safety guarantees.

External Transmission

Medium
Category
Data Exfiltration
Content
}).encode()

    req = urllib.request.Request(
        "https://api.openai.com/v1/images/generations",
        data=payload,
        headers={
            "Authorization": f"Bearer {api_key}",
Confidence
60% 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
}).encode()

    req = urllib.request.Request(
        "https://api.openai.com/v1/images/generations",
        data=payload,
        headers={
            "Authorization": f"Bearer {api_key}",
Confidence
60% 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
}).encode()

    req = urllib.request.Request(
        "https://api.openai.com/v1/images/generations",
        data=payload,
        headers={
            "Authorization": f"Bearer {api_key}",
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Static analysis

No suspicious patterns detected.