SSRF filter bypass: is_global misses IPv6 transition addresses (NAT64/IPv4-compat) → cloud metadata
- This is actually a bug report.
- I am not getting good LLM Results
- I have tried asking for help in the community on discord or discussions and have not received a response.
- I have tried searching the documentation and have not found an answer.
What Model are you using?
- Other: N/A — provider-agnostic. The bug is in the remote-media SSRF guard
(
instructor/v2/core/remote.py), independent of the model/provider.
Describe the bug
The remote-media fetch guard added in #2569 validates addresses with
ipaddress.is_global in _validate_public_address:
if not parsed.is_global or parsed.is_multicast:
raise RemoteFetchError(...)CPython classifies IPv6 transition addresses that embed an internal IPv4 as
globally routable, so the guard passes them. On a host with NAT64/DNS64 routing
(IPv6-only AWS/GCP subnets, many mobile/corporate networks) or dual-stack, the
request is delivered to the embedded IPv4 — loopback, RFC1918, or the cloud
metadata endpoint 169.254.169.254 — enabling SSRF and IAM-credential theft.
| Host | Really targets | Current guard |
|---|---|---|
64:ff9b::a9fe:a9fe |
169.254.169.254 (metadata) | ❌ accepted |
64:ff9b::7f00:1 |
127.0.0.1 | ❌ accepted |
64:ff9b::a00:1 |
10.0.0.1 | ❌ accepted |
::a9fe:a9fe |
169.254.169.254 | ❌ accepted |
::ffff:169.254.169.254 |
169.254.169.254 | ✅ blocked |
::1 / 127.0.0.1 |
loopback | ✅ blocked |
The most important vector is the 64:ff9b::/96 NAT64 well-known prefix, which
real DNS64/NAT64 deployments use. The existing tests cover ::1 but never the
transition prefixes. (Same class as pydantic-ai CVE-2026-46678 and the fixes in
ragflow/lightrag/homebox.)
To Reproduce
from instructor.v2.core import remote
remote._validate_public_url("http://[64:ff9b::a9fe:a9fe]/latest/meta-data/")
# -> returns None (accepted) instead of raising RemoteFetchErrorReachable via public API
from instructor.v2.core.multimodal import Image
img = Image.from_url("http://[64:ff9b::a9fe:a9fe]/x.png") # accepted
# On a NAT64 host, img.to_anthropic()/.to_genai() then fetch the metadata IP.Expected behavior
The guard should judge an IPv6 transition address by the IPv4 it embeds, not by the wrapper. Unwrap IPv4-mapped, 6to4, Teredo, NAT64 (64:ff9b::/96, 64:ff9b:1::/48) and the deprecated IPv4-compatible ::/96 form, and reject the request when the embedded IPv4 is non-global. A NAT64 wrapper around a public IPv4 must stay allowed so IPv6-only egress keeps working.
Screenshots N/A
Source: 567-labs/instructor