T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/news_digest_v2/fetcher.py:734
- Finding
- Unrestricted URL Fetching Enables Server-Side Request Forgery<![CDATA[ ## Vulnerability Details **File Location**: `scripts/news_digest_v2/fetcher.py:734-750`, `scripts/news_digest_v2/fetcher.py:785`, and `scripts/news_digest_v2/fetcher.py:995` **Vulnerability Type**: Server-Side Request Forgery through unvalidated source and article URLs **Risk Level**: High ### Vulnerable Code ```python for a in soup.find_all('a', href=True): url = a['href'] title = a.get_text(strip=True) if not title or len(title) < 5: continue # Convert to an absolute URL if url.startswith('//'): url = 'https:' + url elif not url.startswith('http'): url = urljoin(base_url, url) if not url.startswith('http'): continue if url in seen_urls: continue seen_urls.add(url) links.append({'title': title, 'url': url}) ``` The resulting URLs and database-configured source URLs are subsequently fetched: ```python def fetch_article_content(url, timeout=8): html = fetch_page(url, timeout=timeout) ``` ```python for site in WEBSITES: html = fetch_page(site['url']) ``` ### Technical Analysis The fetch pipeline accepts URLs from two trust boundaries: 1. Source URLs loaded from the configurable `monitor_websites` SQLite table. 2. Absolute links extracted from the HTML of monitored pages. Before issuing requests, the code does not validate: - The URL scheme. - The destination hostname. - The resolved IP address. - Whether the destination is loopback, private, link-local, multicast, or otherwise reserved. - Whether an article URL remains on an approved news-source domain. - The destination of HTTP redirects. - The destination port. The check that a URL begins with `http` is not a sufficient security boundary. It still permits requests to addresses such as `http://127.0.0.1`, private network services, or cloud metadata endpoints. The `requests` library also follows redirects by default, so an initially acceptable URL could redirect to a restricted address unless e ...[truncated 1480 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Permit only `https` URLs unless a narrowly documented exception is essential. 2. Maintain an explicit allowlist of approved news-source hostnames. 3. Require extracted article links to remain on the source hostname or a specifically approved related hostname. 4. Parse every URL with `urllib.parse.urlsplit()` and reject embedded credentials, unexpected ports, malformed hosts, and non-HTTP schemes. 5. Resolve the hostname before connecting and reject all loopback, private, link-local, multicast, unspecified, and reserved IPv4 and IPv6 ranges using Python's `ipaddress` module. 6. Protect against DNS rebinding by ensuring the validated address is the address actually used for the connection. 7. Disable automatic redirects or validate the scheme, hostname, port, and resolved address at every redirect hop. 8. Apply strict response-size and content-type limits before reading response bodies. 9. Treat the SQLite source table as security-sensitive configuration and restrict who can modify the database. 10. Keep the existing response truncation as defense in depth, but do not rely on it as an SSRF control. ]]>
