SSRF Alert in Link Preview Service
Acme Docs has a Python Django service that renders link previews. A CodeQL alert flags requests.get(user_url, timeout=3, allow_redirects=True) in preview/views.py. The URL comes from POST /api/preview JSON url. Before the call, validate_url() parses with urllib.parse.urlparse, requires scheme http or https, rejects hostnames in a static blocklist of 169.254.169.254, localhost, and 127.0.0.1, and resolves DNS once with socket.gethostbyname to reject private IPs. The service runs in EKS with an IRSA role that can read one S3 bucket, preview-assets-prod, and no other permissions. A prior pen test found no SSRF. The engineering team says the blocklist and DNS check make the alert a false positive. The alert is due today; the service handles 8,000 previews per day, and the security champion wants to close it as 'not exploitable'.
1. Is this a false positive? Walk through at least two bypasses and the actual impact.
Read the worked answer
Not a false positive. The validator checks only the initial URL, then requests.get follows redirects. An attacker can submit a public URL that 302-redirects to http://169.254.169.254/latest/meta-data/ or to an internal service; the redirect target is never validated. Second, the DNS check resolves once, but requests resolves again at connect time. An attacker-controlled hostname can return a public IP during validation and a private IP milliseconds later (DNS rebinding). The blocklist also misses IPv6 link-local, decimal and octal encodings, and non-HTTP schemes if parsing is loose. Impact is constrained but real: the pod's IRSA role is limited to one S3 bucket, but SSRF can reach cluster services, the kubelet, and possibly the node metadata endpoint depending on network policy. The prior pen test only shows it did not find this path. Rate it medium-to-high in a shared cluster and fix before closing.
2. Outline a patch that closes redirect and rebinding bypasses.
Read the worked answer
Use an allowlist of schemes (HTTPS only) and ports (443). Resolve the hostname to all A and AAAA records; reject if any is private, loopback, link-local, multicast, or metadata. Connect to the validated IP directly by passing a custom adapter or urllib3 connection pool that pins the IP, and set the Host header to the original hostname so TLS SNI and certificate validation still work. Disable automatic redirects (allow_redirects=False); if redirects are required, validate each hop with the same routine and cap at two hops. Add an egress NetworkPolicy that denies RFC1918, 169.254.0.0/16, and fd00::/8, with an allowlist proxy for approved public destinations. Put the fetch behind a queue with a short timeout, limit response size to 2 MB, and strip active content. Add unit tests for redirect-to-metadata, DNS rebinding, IPv6, and userinfo tricks.
3. What compensating control would you accept for 48 hours if the patch cannot ship today?
Read the worked answer
For 48 hours, disable link preview fetching entirely or restrict it to a short allowlist of partner domains. If fetching must continue, set allow_redirects=False at the HTTP client and add an egress deny for link-local, RFC1918, and IPv6 unique-local ranges; that combination blocks the easiest redirect and metadata paths even if DNS rebinding remains. Add gateway rate limiting and log every fetched URL with the resolved IP, response status, and redirect chain. Alert on any request to 169.254.169.254, 169.254.170.2, or private ranges. Accepting only the static blocklist is not enough because the code already has it. Treat the 48-hour window as risk acceptance with a named owner, not as closure of the CodeQL alert.