T09 · Insecure Skill Coding Practices
- Location
wechat_reader_ocr.py:163- Finding
Server-Side Request Forgery Through Unrestricted Article URL
- Content
View full analysis
str: """Normalize a WeChat article URL.""" if not re.match(self.base_url_pattern, url): return url ``` ```python def read_article(self, original_url: str) -> Dict[str, Any]: try: # Normalize URL normalized_url = self.normalize_url(original_url) # Retrieve and parse the page using requests try: resp = requests.get(normalized_url, timeout=15, headers={ 'User-Agent': 'Mozilla/5.0 (compatible; wechat-look/1.0)' }) resp.raise_for_status() html = resp.text ``` ### Technical Analysis The implementation defines a regular expression intended to recognize WeChat article URLs. However, `normalize_url()` does not reject a URL that fails this check. Instead, it returns the untrusted URL unchanged. `read_article()` subsequently passes that returned value directly to `requests.get()`. Consequently, the documented restriction to WeChat article URLs is not enforced at the network boundary. An attacker can supply an arbitrary URL using HTTP or HTTPS, including URLs resolving to: - Loopback services such as `127.0.0.1` or `::1` - Private network services - Link-local services - Cloud instance metadata endpoints - Internal administrative interfaces - Other services reachable only from the Skill host The implementation also uses the default redirect behavior of `requests`, so an initially acceptable-looking endpoint could redirect the request to an internal destination unless every redirect target is independently validated. ### Attack Path 1. An attacker invokes the Skill with a URL such as `http://127.0.0.1:8080/`, a private-network address, or a cloud metadata end ...[truncated 1219 chars]- Remediation
View remediation
None: parsed = urlparse(url) if parsed.scheme != "https": raise ValueError("Only HTTPS URLs are permitted") if parsed.hostname != "mp.weixin.qq.com": raise ValueError("Only mp.weixin.qq.com URLs are permitted") if not parsed.path.startswith("/s/"): raise ValueError("Unsupported WeChat article path") if parsed.username or parsed.password: raise ValueError("URL credentials are not permitted") for entry in getaddrinfo(parsed.hostname, 443): address = ip_address(entry[4][0]) if ( address.is_private or address.is_loopback or address.is_link_local or address.is_multicast or address.is_reserved or address.is_unspecified ): raise ValueError("The destination resolves to a prohibited address") ``` DNS resolution checks should be combined with redirect validation and network-level egress controls to reduce DNS re ...[truncated 50 chars]
