T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/geizhals.py:184
- Finding
- Unrestricted Candidate URL Fetching Enables Server-Side Request Forgery## Vulnerability Details **File Location**: `scripts/geizhals.py:184-190` and `scripts/geizhals.py:250-257` **Vulnerability Type**: Server-Side Request Forgery (SSRF) through insufficient URL validation **Risk Level**: Medium ### Vulnerable Code ```python def candidate_to_url(candidate: list[Any]) -> str | None: if not candidate: return None first = candidate[0] if len(candidate) > 0 else None if isinstance(first, str) and first: if first.startswith("http"): return first if first.startswith("/"): return BASE + first return f"{BASE}/{first}" return None ``` The accepted URL is subsequently fetched without destination validation: ```python detail_url = candidate_to_url(row) if not detail_url: continue item: dict[str, Any] = { "schema_version": SCHEMA_VERSION, "name": name, "detail_url": detail_url, "min_price_eur": None, "offer_count": None, "shop": None, "price_confidence": "unknown", "price_source": "none", "error": None, } try: page = fetch_text( detail_url, extra_headers={"Accept": "text/html,*/*"}, retries=3, backoff_base=0.6, cache_dir=cache_dir, debug=debug, ) ``` ### Technical Analysis Autocomplete candidates are obtained from a remote JSON response. `candidate_to_url()` trusts any candidate string beginning with `http`, without parsing and validating its scheme, hostname, port, resolved address, or final redirect destination. The resulting URL is passed to `urllib.request.urlopen()` through `fetch_text()`. Python's URL opener follows HTTP redirects by default, and the implementation does not validate redirect targets. Consequently, a malicious or compromised autocomplete response—or a permitted destination that redirects elsewhere—could instruct the Skill to issue HTTP requests to arbitrary destinations reachable from the Agent environment. Potential targets include: - Loopback ...[truncated 2296 chars]
- Remediation
- ## Remediation Suggestions 1. **Reject arbitrary absolute candidate URLs.** Prefer accepting only relative Geizhals product paths returned by the autocomplete endpoint. 2. **Apply an explicit destination allowlist.** Parse URLs with `urllib.parse.urlsplit()` and require: - The `https` scheme - An exact approved hostname, such as `geizhals.at` - Only explicitly required subdomains - No embedded username or password - An approved port, normally port 443 3. **Validate resolved addresses.** Resolve the hostname and reject every address classified as loopback, private, link-local, multicast, unspecified, or reserved. Apply the validation immediately before connecting to reduce DNS rebinding risk. 4. **Control redirects.** Disable automatic redirects or implement a redirect handler that validates every redirect target using the same scheme, hostname, port, and resolved-address rules. Limit the number of redirects. 5. **Do not rely on prefix checks.** A check such as `startswith("http")` is not a security boundary and can admit unintended schemes or hosts. Use structured URL parsing and exact comparisons. 6. **Add negative security tests.** Test rejection of: - `http://127.0.0.1/` - `http://[::1]/` - Private IPv4 and IPv6 destinations - Link-local metadata addresses - URLs containing user information - Unapproved ports - Approved URLs that redirect to internal destinations - Hostnames resolving to private or mixed public/private addresses 7. **Restrict cached content.** Ensure the cache directory has restrictive permissions and consider avoiding caching responses unless the final destination has passed all validation.
