T09 · Insecure Skill Coding Practices
- Location
scripts/geizhals.py:184- Finding
Unrestricted Candidate URL Fetching Enables Server-Side Request Forgery
- Content
View full analysis
Vulnerability Details
File Location:
scripts/geizhals.py:184-190andscripts/geizhals.py:250-257
Vulnerability Type: Server-Side Request Forgery (SSRF) through insufficient URL validation
Risk Level: MediumVulnerable 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 NoneThe 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 withhttp, without parsing and validating its scheme, hostname, port, resolved address, or final redirect destination.The resulting URL is passed to
urllib.request.urlopen()throughfetch_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
View remediation
Remediation Suggestions
-
Reject arbitrary absolute candidate URLs. Prefer accepting only relative Geizhals product paths returned by the autocomplete endpoint.
-
Apply an explicit destination allowlist. Parse URLs with
urllib.parse.urlsplit()and require:- The
httpsscheme - An exact approved hostname, such as
geizhals.at - Only explicitly required subdomains
- No embedded username or password
- An approved port, normally port 443
- The
-
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.
-
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.
-
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. -
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
-
Restrict cached content. Ensure the cache directory has restrictive permissions and consider avoiding caching responses unless the final destination has passed all validation.
-
