Back to skill

Security audit

zeroHub Image API

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its stated zeroHub image-generation purpose, but it has review-worthy network and credential scoping gaps around redirects, configurable API origins, and opt-in relaxed download controls.

Install only if you are comfortable with this Review-level risk. Use it with the default zeroHub endpoint, avoid ZEROHUB_BASE_URL unless you fully trust the alternate host, do not enable --allowed-host or --allow-http for untrusted URLs, and choose an output directory that is private to your user. Treat ZEROHUB_API_KEY as a billing credential and confirm image-generation requests before running them.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/zerohub_image_public.py:121
Finding

Automatic redirects bypass API and download destination validation

Content
View full analysis
Dict[str, Any]: key = _api_key(True) origin = _base_origin() url = path if path.startswith(("http://", "https://")) else f"{origin}{path}" # API requests must go to the configured zeroHub origin only. This prevents accidental # credential transmission to arbitrary URLs if a path is attacker-controlled. parsed_url = urllib.parse.urlparse(url) parsed_origin = urllib.parse.urlparse(origin) if parsed_url.scheme != "https" or parsed_url.hostname != parsed_origin.hostname: return {"error": "API request host is not allowed", "url": url} body = None headers = {"Authorization": f"Bearer {key}", "Accept": "application/json"} if data is not None: body = json.dumps(data, ensure_ascii=False).encode("utf-8") headers["Content-Type"] = "application/json" req = urllib.request.Request(url, data=body, headers=headers, method=method.upper()) try: with urllib.request.urlopen(req, timeout=timeout) as resp: ``` ```python abs_url = _absolute_url(url) try: safe_url = _validate_url_for_network( abs_url, allowed_hosts=_allowed_hosts(allowed_hosts), allow_http=allow_http, ) except Exception as e: # noqa: BLE001 return {"ok": False, "url": abs_url, "error": str(e)} output_dir.mkdir(parents=True, exist_ok=True) req = urllib.request.Request(safe_url, headers={"User-Agent": "zerohub-image-api/1.0"}) try: with urllib.request.urlopen(req, timeout=timeout) as resp: ``` ### Technical Analysis The code validates only the initial request ...[truncated 2302 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/zerohub_image_public.py:86
Finding

DNS rebinding can bypass private-network download protection

Content
View full analysis
None: try: infos = socket.getaddrinfo(host, None, proto=socket.IPPROTO_TCP) except socket.gaierror as e: raise ValueError(f"cannot resolve host {host}: {e}") from e seen: set[str] = set() for info in infos: ip_text = info[4][0] if ip_text in seen: continue seen.add(ip_text) if _is_blocked_ip(ip_text): raise ValueError(f"blocked private or local network target: {host} -> {ip_text}") def _validate_url_for_network(url: str, *, allowed_hosts: set[str], allow_http: bool = False) -> str: parsed = urllib.parse.urlparse(url) if parsed.scheme not in ({"https", "http"} if allow_http else {"https"}): raise ValueError("only https URLs are allowed" if not allow_http else "only http/https URLs are allowed") host = (parsed.hostname or "").lower().rstrip(".") _validate_hostname(host, allowed_hosts) _validate_resolved_ips(host) return urllib.parse.urlunparse(parsed) ``` ```python abs_url = _absolute_url(url) try: safe_url = _validate_url_for_network( abs_url, allowed_hosts=_allowed_hosts(allowed_hosts), allow_http=allow_http, ) except Exception as e: # noqa: BLE001 return {"ok": False, "url": abs_url, "error": str(e)} output_dir.mkdir(parents=True, exist_ok=True) req = urllib.request.Request(safe_url, headers={"User-Agent": "zerohub-image-api/1.0"}) try: with urllib.request.urlopen(req, timeout=timeout) as resp: ``` ### Technical Analysis The hostname is resolved once by `_validate_resolved_ips()` for policy validation. The validated IP address is not retaine ...[truncated 1945 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/zerohub_image_public.py:219
Finding

Non-atomic output-file creation permits symlink race attacks

Content
View full analysis
Remediation
View remediation
Vulnerability Patterns
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
Findings (7)

Tainted flow: 'req' from os.environ.get (line 137, 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/zerohub_image_public.py (reported line 139)May include surrounding context.

python
headers["Content-Type"] = "application/json"
    req = urllib.request.Request(url, data=body, headers=headers, method=method.upper())
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            raw = resp.read().decode("utf-8", errors="replace")
            try:
                parsed = json.loads(raw) if raw else {}

Tainted flow: 'req' from os.environ.get (line 137, 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/zerohub_image_public.py (reported line 223)May include surrounding context.

python
headers["Content-Type"] = "application/json"
    req = urllib.request.Request(url, data=body, headers=headers, method=method.upper())
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            raw = resp.read().decode("utf-8", errors="replace")
            try:
                parsed = json.loads(raw) if raw else {}

Tp2

High
Category
MCP Tool Poisoning
Confidence
85% confidence
Finding

Mixing characters from multiple Unicode scripts in a single identifier is a common technique to create visually ambiguous tool names.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding

The skill describes capabilities to read an API key from the environment, access the network, and write downloaded files locally, but it does not declare an explicit tool scope such as permissions or allowed-tools. That creates a governance gap: reviewers and enforcement systems cannot reliably constrain what the skill is allowed to do, increasing the risk of unintended secret access, outbound requests, or filesystem writes if the implementation changes or is misused.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The manifest description is written entirely in Chinese and presents the skill as Chinese-language-facing, with no indication that users can opt into another language or locale. Under the stated policy, forcing a specific language without user choice is a natural-language policy violation unless the locale restriction is explicitly justified.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

The security claim says the API key is only sent to the configured zeroHub API origin, but that origin is fully controlled by the ZEROHUB_BASE_URL environment variable. If an attacker can influence the runtime environment or deployment config, they can redirect authenticated requests and capture the user's API key while the code still appears to honor its documented policy.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The module documentation and metadata present downloads as restricted to zeroHub hosts over HTTPS, but the CLI exposes --allowed-host and --allow-http, which let callers weaken both protections. In an agent skill context, this can expand the tool into a general network fetcher and make SSRF, unsafe plaintext transport, or policy-bypass scenarios more likely if an upstream agent or prompt can influence arguments.

Content

No source excerpt is available for this finding.