T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/design_collector.py:23
- Finding
- Hostname Substring Matching Allows URL Allowlist Bypass<![CDATA[ ## Vulnerability Details **File Location**: `scripts/design_collector.py`, lines 23-57 **Vulnerability Type**: Improper URL validation **Risk Level**: Medium ### Vulnerable Code ```python def is_valid_dribbble_url(url: str) -> bool: if "dribbble.com" not in url: return False allowed_patterns = [ r"dribbble\.com/search/", r"dribbble\.com/tags/", r"dribbble\.com/shots/popular", ] for p in allowed_patterns: if re.search(p, url): return True return False def is_valid_pinterest_url(url: str) -> bool: if "pinterest.com" not in url: return False allowed_patterns = [ r"pinterest\.com/search/pins", r"pinterest\.com/search/\?", r"pinterest\.com/ideas/", ] for p in allowed_patterns: if re.search(p, url): return True return False ``` ### Technical Analysis The URL validators search for trusted-domain strings and path fragments anywhere in the complete URL. They do not parse the URL or verify that the actual hostname is an approved Dribbble or Pinterest hostname. Consequently, an attacker-controlled URL can satisfy the checks merely by embedding a trusted-looking string in its hostname or path. Examples include: ```text https://dribbble.com.attacker.example/search/healthcare https://attacker.example/dribbble.com/search/healthcare https://pinterest.com.attacker.example/search/pins?q=design ``` These URLs are controlled by `attacker.example`, but the substring and regular-expression checks can accept them. Dribbble URLs received from Tavily are passed through this validation before being written to the generated Markdown and JSON reports. ### Attack Path 1. An attacker creates a page whose URL contains an allowed domain and path string but whose actual hostname is attacker-controlled. 2. The attacker causes the page to appear in search-engine results for one of the queries used by the Skill. 3. Tavily returns the m ...[truncated 814 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions Parse each URL and validate its components independently: 1. Use `urllib.parse.urlparse`. 2. Require the `https` scheme. 3. Normalize the hostname to lowercase and remove any trailing dot. 4. Compare the hostname against an explicit set of permitted hostnames. 5. Validate the parsed path with anchored rules. 6. Reject URLs containing credentials, malformed ports, or unexpected hostnames. 7. Revalidate URLs immediately before report generation. Example: ```python from urllib.parse import urlparse def is_valid_dribbble_url(url: str) -> bool: try: parsed = urlparse(url) host = (parsed.hostname or "").lower().rstrip(".") if parsed.scheme != "https": return False if host not in {"dribbble.com", "www.dribbble.com"}: return False return ( parsed.path.startswith("/search/") or parsed.path.startswith("/tags/") or parsed.path == "/shots/popular" or parsed.path.startswith("/shots/popular/") ) except ValueError: return False def is_valid_pinterest_url(url: str) -> bool: try: parsed = urlparse(url) host = (parsed.hostname or "").lower().rstrip(".") if parsed.scheme != "https": return False if host not in {"pinterest.com", "www.pinterest.com"}: return False return ( parsed.path == "/search/pins/" or parsed.path.startswith("/ideas/") ) except ValueError: return False ``` Add negative tests for deceptive hostnames, user-information syntax, mixed-case hosts, trailing-dot hosts, alternate ports, and trusted-domain strings embedded in paths. ]]>
