Back to skill

Security audit

Mixtiles It

Security checks for vulnerabilities and agentic risk

Overview

The skill matches its photo-ordering purpose, but it can fetch arbitrary URLs and publicly upload images to Cloudinary without a strong confirmation boundary.

Review before installing. Use this only for photos you are comfortable uploading to Cloudinary and sending to Mixtiles, and avoid sensitive images or private/internal URLs. The publisher should add an explicit consent step, stronger URL-fetch protections, image validation, and download size limits.

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/mixtiles-cart.py:56
Finding

SSRF Protection Can Be Bypassed Through Redirects or DNS Rebinding

Content
View full analysis
None: """Block private/internal URLs to prevent SSRF.""" import ipaddress, socket parsed = urllib.parse.urlparse(url) if parsed.scheme not in ("http", "https"): raise ValueError(f"Unsupported scheme: {parsed.scheme}") hostname = parsed.hostname or "" if not hostname: raise ValueError("No hostname in URL") # Block obvious internal hostnames blocked = ("localhost", "127.0.0.1", "0.0.0.0", "metadata.google.internal", "169.254.169.254") if hostname.lower() in blocked or hostname.lower().endswith(".local"): raise ValueError(f"Blocked internal hostname: {hostname}") # Resolve and check for private IPs try: for info in socket.getaddrinfo(hostname, None): addr = ipaddress.ip_address(info[4][0]) if addr.is_private or addr.is_loopback or addr.is_link_local or addr.is_reserved: raise ValueError(f"URL resolves to private/reserved IP: {addr}") except socket.gaierror: raise ValueError(f"Cannot resolve hostname: {hostname}") def download_to_temp(source_url: str) -> str: """Download a remote image to a temp file. Returns the temp file path.""" import tempfile _validate_url(source_url) req = urllib.request.Request(source_url, headers={ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)" }) with urllib.request.urlopen(req, timeout=30) as resp: ``` ### Technical Analysis The function validates the hostname and its resolved addresses before passing the original URL to `urllib.request.urlopen`. This creates a time-of-check/time-of-use separation between validation and connection. `urlopen` can automatically follow HTTP redirects, but the redirect destination is not passed th ...[truncated 2230 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Note
Location
scripts/mixtiles-cart.py:80
Finding

Unbounded Remote Response Download Enables Resource Exhaustion

Content
View full analysis
str: """Download a remote image to a temp file. Returns the temp file path.""" import tempfile _validate_url(source_url) req = urllib.request.Request(source_url, headers={ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)" }) with urllib.request.urlopen(req, timeout=30) as resp: content_type = resp.headers.get("Content-Type", "image/jpeg") ext = mimetypes.guess_extension(content_type.split(";")[0].strip()) or ".jpg" tmp = tempfile.NamedTemporaryFile(suffix=ext, delete=False) tmp.write(resp.read()) tmp.close() return tmp.name ``` ### Technical Analysis `resp.read()` reads the complete remote response into memory without enforcing a maximum byte count. The resulting object is then written to disk, causing both memory and temporary-storage consumption. The 30-second network timeout does not provide a response-size limit. A remote server may return a very large response quickly enough to remain within the timeout. The implementation also trusts the remote `Content-Type` header when selecting a filename suffix but does not verify that the response is an actual image. Arbitrary content can therefore be downloaded and passed to the public upload service. If an exception occurs after creation of the temporary file but before the function returns, the caller does not receive its path and may not remove the partial file. ### Attack Path 1. An attacker provides a URL presented as an image. 2. The URL passes the network-address checks. 3. The attacker-controlled server returns a very large response or streams data until runtime resources are exhausted. 4. `resp.read()` accumulates the response in process memory. 5. If ...[truncated 826 chars]
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • 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
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (8)

Missing User Warnings

High
Category
Not specified by scanner
Confidence
97% confidence
Finding

The skill states that local files are uploaded to Cloudinary and then used to generate Mixtiles cart links, but it does not warn users up front that their local files and provided URLs are sent to third-party services. This creates a meaningful privacy and data-handling risk, especially if users provide personal photos, sensitive images, or internal URLs they do not realize will be transmitted externally.

Content

No source excerpt is available for this finding.

Cloud Metadata Access

High
Category
Server-Side Request Forgery
Confidence
90% confidence
Finding

Code accesses a cloud instance metadata endpoint (e.g. 169.254.169.254). A single request can return temporary IAM credentials, making this a high-value SSRF target for credential theft.

Content

Scanner excerpt · scripts/mixtiles-cart.py (reported line 64)May include surrounding context.

python
if not hostname:
        raise ValueError("No hostname in URL")
    # Block obvious internal hostnames
    blocked = ("localhost", "127.0.0.1", "0.0.0.0", "metadata.google.internal", "169.254.169.254")
    if hostname.lower() in blocked or hostname.lower().endswith(".local"):
        raise ValueError(f"Blocked internal hostname: {hostname}")
    # Resolve and check for private IPs

Cloud Metadata Access

High
Category
Server-Side Request Forgery
Confidence
90% confidence
Finding

Code accesses a cloud instance metadata endpoint (e.g. 169.254.169.254). A single request can return temporary IAM credentials, making this a high-value SSRF target for credential theft.

Content

Scanner excerpt · scripts/mixtiles-cart.py (reported line 64)May include surrounding context.

python
if not hostname:
        raise ValueError("No hostname in URL")
    # Block obvious internal hostnames
    blocked = ("localhost", "127.0.0.1", "0.0.0.0", "metadata.google.internal", "169.254.169.254")
    if hostname.lower() in blocked or hostname.lower().endswith(".local"):
        raise ValueError(f"Blocked internal hostname: {hostname}")
    # Resolve and check for private IPs

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding

The skill invokes shell commands, accesses environment variables, and performs network operations, but it declares no explicit tool scope or permission boundaries. That makes the skill harder to constrain and audit, increasing the chance it can access more capabilities than a user or host expects.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The invocation text includes broad natural-language triggers like 'mixtile this' and 'make this a tile,' which can cause the skill to activate on ambiguous user messages or forwarded content. In this skill, mistaken activation is more dangerous because activation can lead to file handling, URL fetching, and third-party uploads without a narrowly scoped confirmation step.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The script explicitly uploads user-provided images to public third-party hosting services so Mixtiles can access them, but the interface and documentation do not provide a clear, explicit warning that the images become publicly accessible URLs. In a photo-ordering skill, users may reasonably expect processing for fulfillment, but not necessarily public exposure on Cloudinary or another host, creating a privacy and data-handling risk.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% 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/mixtiles-cart.py (reported line 26)May include surrounding context.

python
DEFAULT_UPLOAD_URL = os.environ.get("MIXTILES_UPLOAD_URL", "")
DEFAULT_UPLOAD_KEY = os.environ.get("MIXTILES_UPLOAD_KEY", "")
MIXTILES_DESIGN_URL = "https://www.mixtiles.com/photos"
CLOUDINARY_UPLOAD_URL = "https://api.cloudinary.com/v1_1/{cloud}/image/upload"


def upload_to_cloudinary(file_path: str, cloud: str = "demo", preset: str = "unsigned") -> str:

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/mixtiles-cart.py (reported line 37)May include surrounding context.

python
Cloudinary URLs only, so this is the only reliable upload target.
    """
    url = CLOUDINARY_UPLOAD_URL.format(cloud=cloud)
    result = subprocess.run(
        ["curl", "-sf",
         "-F", f"file=@{file_path}",
         "-F", f"upload_preset={preset}",

Static analysis

No suspicious patterns detected.