Skip to content

fix(fetch_url): block cloud-metadata via IPv4-mapped IPv6 in SSRF guard#463

Open
steps-re wants to merge 1 commit into
aliasrobotics:mainfrom
steps-re:fix/ssrf-ipv4-mapped-metadata-bypass
Open

fix(fetch_url): block cloud-metadata via IPv4-mapped IPv6 in SSRF guard#463
steps-re wants to merge 1 commit into
aliasrobotics:mainfrom
steps-re:fix/ssrf-ipv4-mapped-metadata-bypass

Conversation

@steps-re

@steps-re steps-re commented Jul 7, 2026

Copy link
Copy Markdown

Summary

The fetch_url SSRF guard can be bypassed to reach cloud-metadata endpoints (AWS/GCP/Azure IMDS) via the IPv4-mapped IPv6 form of the address.

Details

_check_ssrf documents that metadata endpoints like 169.254.169.254 are "ALWAYS blocked, regardless of CAI_FETCH_ALLOW_INTERNAL." That block was a string comparison against the bare IPv4 literal, so it could be evaded:

  • http://[::ffff:169.254.169.254]/
  • http://[::ffff:a9fe:a9fe]/ (hex form)

CAI_FETCH_ALLOW_INTERNAL=true is a documented mode for authorized internal pentests; in that mode the private-range check is intentionally skipped, leaving the metadata block as the only protection — which this bypasses.

Fix

Add _unwrap_mapped_ip() to unwrap IPv4-mapped IPv6 addresses to their embedded IPv4 before the metadata and private-range checks, in both the literal-IP and DNS-resolved paths. This also hardens the private-range check on Python versions that don't classify mapped addresses as private/link-local. Behavior for allowed/public addresses is unchanged.

Verification

  • Editable install with [test] extras, Python 3.13.
  • Before: with allow_internal=True, _check_ssrf("http://[::ffff:169.254.169.254]/") returned ALLOWED.
  • After: mapped metadata forms BLOCKED in both allow_internal states; mapped loopback blocked by default; public IPs still allowed.
  • pytest tests/tools/web/test_fetch_url.py71 passed (69 baseline + 2 new regression tests). ruff==0.9.2 clean on changed files.

Framing: this is reachable only when the operator has opted into CAI_FETCH_ALLOW_INTERNAL, so it hardens the explicit "metadata always blocked" guarantee rather than a remote-by-default exploit.

The fetch_url SSRF guard guarantees cloud-metadata endpoints (e.g.
169.254.169.254) are ALWAYS blocked, even when CAI_FETCH_ALLOW_INTERNAL
is set for an authorised internal pentest. That guarantee could be
bypassed with the IPv4-mapped IPv6 form of the IMDS address, e.g.
http://[::ffff:169.254.169.254]/ (or its hex form ::ffff:a9fe:a9fe),
because the metadata block is a plain string comparison against the bare
IPv4 literal. With allow_internal=True the private/reserved check is
skipped, so nothing caught the mapped form and the request reached IMDS.

Unwrap IPv4-mapped IPv6 addresses to their embedded IPv4 before the
metadata and private-range checks in both the literal-IP and DNS-resolved
paths. This also hardens the private-range check on Python versions that
do not classify mapped addresses as private/link-local.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Mike German <mike@stepsventures.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant