T09 · Insecure Skill Coding Practices
- Location
scripts/design_collector.py:23- Finding
Hostname Substring Matching Allows URL Allowlist Bypass
- Content
View full analysis
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
View remediation
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. ]]>
