T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/audit_page.py:138
- Finding
- Server-Side Request Forgery Through Unvalidated Redirect Destinations<![CDATA[ ## Vulnerability Details **File Location**: `scripts/audit_page.py`, lines 138–153 and 286–299 **Vulnerability Type**: Server-Side Request Forgery (SSRF) through unvalidated redirects **Risk Level**: High ### Vulnerable Code ```python def fetch(url): session = requests.Session() session.headers.update({"User-Agent": UA}) response = session.get(url, timeout=TIMEOUT, allow_redirects=True) response.raise_for_status() return response, session def fetch_optional(session, url): try: r = session.get(url, timeout=TIMEOUT, allow_redirects=True) return { "url": r.url, "status": r.status_code, "ok": 200 <= r.status_code < 300, "text": r.text[:4000], } except Exception as e: return {"url": url, "ok": False, "error": str(e)} ``` The initial target validation is performed only before `fetch()`: ```python url = sys.argv[1] blocked, reason = is_blocked_target(url) if blocked: print( f"Refusing to audit target by default: {reason}. Use only public HTTP/HTTPS URLs for routine SEO audits.", file=sys.stderr, ) sys.exit(3) response, session = fetch(url) final_url = response.url soup = BeautifulSoup(response.text, "html.parser") ``` ### Technical Analysis The script attempts to prevent SSRF by rejecting private, loopback, link-local, reserved, multicast, and known metadata destinations in `is_blocked_target()`. The strings `metadata.google.internal` and `169.254.169.254` are denylist entries and do not themselves represent intentional metadata access. However, both network request functions use `allow_redirects=True`. The supplied URL is validated only once, before the first request. The `requests` library then follows HTTP redirects automatically without passing each redirect destination through `is_blocked_target()`. Consequently, an attacker-controlled public server can pass the initial validation and return a redirect to: - ...[truncated 2936 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. **Disable automatic redirects** Set `allow_redirects=False` for every request and process redirects manually. 2. **Validate every redirect destination** Resolve relative `Location` headers with `urljoin()`, then apply `is_blocked_target()` before issuing the next request. Reject redirects to private, loopback, link-local, reserved, multicast, and metadata destinations. 3. **Enforce a strict redirect limit** Permit only a small number of redirects, such as five, and reject loops. 4. **Restrict URL schemes** Explicitly allow only `http` and `https`. Reject URLs containing credentials and reject all unsupported schemes. 5. **Revalidate final destinations** Validate `response.url` before parsing content or using it to construct robots and sitemap URLs. 6. **Mitigate DNS rebinding** Avoid resolving a hostname during validation and then independently resolving it during connection. Use a networking layer that connects to a previously validated IP address while preserving the expected HTTP Host header and TLS SNI, or apply equivalent connection-time IP enforcement. 7. **Apply identical protections to all requests** Page, robots, sitemap, and sitemap-index requests must share the same redirect, DNS, scheme, and destination validation logic. 8. **Constrain responses** Stream responses and enforce maximum byte limits before downloading complete bodies. Accept only expected textual content types and avoid returning raw internal-looking response bodies. A safe redirect loop should follow this general pattern: ```python def safe_get(session, url, max_redirects=5): current = url for _ in range(max_redirects + 1): blocked, reason = is_blocked_target(current) if blocked: raise ValueError(f"Blocked request destination: {reason}") response = session.get( current, timeout=TIMEOUT, allow_redirects=False, stream=T ...[truncated 642 chars]
