T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/fetch.py:132
- Finding
- Unrestricted Server-Side Request Forgery and Redirect-Based Allowlist Bypass<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fetch.py:33-40`, `scripts/fetch.py:102-109`, `scripts/fetch.py:132-144`, and `scripts/fetch.py:195-207` **Vulnerability Type**: Server-Side Request Forgery (SSRF) and incomplete destination validation **Risk Level**: High ### Vulnerable Code ```python def is_domain_allowed(url: str) -> bool: """Check whether the domain is on the allowlist.""" if not ALLOWED_DOMAINS: return True parsed = urlparse(url) allowed = [d.strip() for d in ALLOWED_DOMAINS.split(",")] return parsed.netloc in allowed or any( parsed.netloc.endswith("." + d) for d in allowed ) ``` ```python def fetch_remote(url: str, max_chars: int = DEFAULT_MAX_CHARS) -> dict: """Remote cleaning mode using Jina Reader.""" try: clean_url = url.replace("https://", "").replace("http://", "") jina_url = JINA_READER_URL.format(url=clean_url) response = requests.get(jina_url, timeout=30) response.raise_for_status() ``` ```python def fetch_local(url: str, max_chars: int = DEFAULT_MAX_CHARS) -> dict: """Local parsing mode.""" try: headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } response = requests.get(url, headers=headers, timeout=30) response.raise_for_status() content = clean_html_local(response.text, max_chars) ``` ```python use_remote = args.remote or (DEFAULT_MODE == "remote" and not args.local) if use_remote: result = fetch_remote(url, args.max_chars) if not result["success"]: print( f"Remote fetch failed: {result['error']}, falling back to local...", file=sys.stderr ) result = fetch_local(url, args.max_chars) else: result = fetch_local(url, args.max_chars) ``` ### Technical Analysis The Skill accepts a caller-provided URL and issues an HTTP request from the Agent environment. When `ALLOWED_DOMAINS ...[truncated 3574 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Require an explicit destination allowlist instead of allowing every domain when configuration is absent. 2. Parse the URL before use and permit only the `http` and `https` schemes. 3. Reject URLs containing embedded credentials unless they are explicitly required. 4. Resolve the hostname and reject every address in loopback, private, link-local, multicast, unspecified, reserved, and other non-public ranges. 5. Explicitly block known cloud metadata destinations, including link-local metadata addresses and provider-specific metadata hostnames. 6. Disable automatic redirects with `allow_redirects=False`, or process redirects manually and apply the complete scheme, hostname, port, DNS, and IP validation policy to every redirect target. 7. Revalidate the resolved destination immediately before connecting to reduce DNS-rebinding exposure. 8. Normalize hostnames using a strict IDNA-aware parser and compare hostname values rather than `netloc`, which can include ports and credentials. 9. Restrict destination ports to those required by the Skill, normally TCP 80 and 443. 10. Apply outbound firewall or sandbox rules so the Skill process cannot reach internal networks or metadata services even if application-level validation fails. 11. Do not automatically fall back from remote retrieval to local retrieval. Require explicit user consent before changing which system contacts the target. 12. Add tests covering direct private addresses, IPv6 loopback, decimal or encoded IP representations, redirects to private addresses, DNS results containing private addresses, embedded credentials, and metadata endpoints. ]]>
