T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/location_service.py:45
- Finding
- Server-Side Request Forgery Through Incomplete Host Validation and Unrestricted Redirects<![CDATA[ ## Vulnerability Details **File Location**: `scripts/location_service.py`, lines 45–58 **Vulnerability Type**: Server-Side Request Forgery (SSRF) **Risk Level**: High ### Vulnerable Code ```python def is_google_maps_url(text): """Check if text looks like a Google Maps URL (standard or short)""" return bool(re.match( r'https?://(maps\.google\.com|www\.google\.com/maps|maps\.app\.goo\.gl)', text.strip() )) def resolve_short_url(url): """Follow redirects on a short URL and return the final URL""" try: req = urllib.request.Request(url, headers={'User-Agent': 'LocationService/1.0'}) with urllib.request.urlopen(req, timeout=10) as resp: return resp.url except Exception as e: raise ValueError(f"Failed to resolve short URL: {e}") ``` The short-link resolution path is invoked by the following logic at lines 73–75: ```python # Resolve short URLs first if 'maps.app.goo.gl' in url: url = resolve_short_url(url) ``` ### Technical Analysis The URL validation uses a regular expression that does not enforce a boundary after the expected hostname. For example, an attacker-controlled URL with a hostname such as: ```text https://maps.app.goo.gl.attacker.example/path ``` matches the `maps.app.goo.gl` prefix and is accepted as a Google Maps URL. The subsequent substring check also treats this URL as a short Google Maps link and passes it to `urllib.request.urlopen`. That API follows HTTP redirects by default. Neither the initial destination nor any redirect destination is validated using parsed hostname equality, DNS resolution checks, or private-address filtering. Consequently, an attacker-controlled endpoint can redirect the request to loopback, link-local, private-network, or cloud metadata addresses. Although the response body is not directly returned, issuing the GET request can still access state-changing internal endpoints and provide a blind SSRF channel through status, timing ...[truncated 1792 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Parse input with `urllib.parse.urlsplit` instead of validating URLs with a prefix regular expression. 2. Require `https` and exact, case-normalized hostname equality: ```python from urllib.parse import urlsplit def is_google_maps_short_url(value): parsed = urlsplit(value.strip()) return ( parsed.scheme == "https" and parsed.hostname is not None and parsed.hostname.lower() == "maps.app.goo.gl" and parsed.username is None and parsed.password is None ) ``` 3. Disable automatic redirects and process each redirect manually. 4. On every redirect, repeat scheme and hostname validation rather than trusting the initial destination. 5. Resolve destination hostnames and reject all loopback, private, link-local, multicast, unspecified, reserved, and otherwise non-public IPv4 and IPv6 addresses. 6. Defend against DNS rebinding by validating resolved addresses immediately before connecting and ensuring the connection uses the validated address. 7. Set strict redirect-count, response-size, and timeout limits. 8. Consider removing server-side short-link resolution entirely if it is not essential. ]]>
