CVE-2026-46678
Pydantic AI: SSRF via IPv6-encoded IPs exposes cloud credentials.
- CVSS 6.8
- 918
- Input Validation and Sanitization
- Remote
Pydantic AI is a Python agent framework for building Generative AI applications. In versions 1.56.0 through 1.98.0, when an application opts a URL into force_download='allow-local' (disabling the default block on private/internal IPs), the cloud-metadata blocklist could be bypassed by encoding the metadata IP in an IPv6 transition form (IPv4-mapped IPv6, 6to4, or NAT64), exposing cloud IAM short-term credentials on dual-stack or translated networks. This is an incomplete fix of GHSA-2jrp-274c-jhv3 / CVE-2026-25580, whose remediation did not hold for IPv6-encoded forms of the metadata IPs. An application is affected only if it explicitly opts a FileUrl (ImageUrl, AudioUrl, VideoUrl, DocumentUrl) into force_download='allow-local' on a URL influenced by untrusted input; it is not affected when using bundled integrations to ingest user input (Agent.to_web / clai web, VercelAIAdapter, AGUIAdapter / Agent.to_ag_ui), since they do not propagate force_download from external data, nor when downloading only from developer-controlled URLs. This issue has been fixed in version 1.99.0.
- CWE
- 918
- CVSS base score
- 6.8
- Published
- 2026-07-29
- OWASP
- A10 Server-Side Request Forgery (SSRF)
- Orthogonal defect classification
- Checking
- Code defect classification
- Incorrect Check
- Category
- Input Validation and Sanitization
- Subcategory
- Server-Side Request Forgery (SSRF)
- Accessibility scope
- Remote
- Impact
- Information Disclosure
- Affected component
- Pydantic AI
- Fixed by upgrading
- Yes
Solution
Upgrade Pydantic AI to version 1.99.0 or later.
Vulnerable code sample
from urllib.parse import urlparse
import socket
# Simplified representation of the cloud-metadata blocklist
CLOUD_METADATA_BLOCKLIST = {"169.254.169.254"}
def _is_url_blocked(url: str) -> bool:
"""
Checks if a URL points to a blocked resource, like a cloud metadata endpoint.
This check is intended to run when force_download='allow-local' is used.
"""
try:
hostname = urlparse(url).hostname
if not hostname:
return False
# Resolve hostname to its IP addresses to handle various formats
addr_infos = socket.getaddrinfo(hostname, None)
resolved_ips = {info[4][0] for info in addr_infos}
for ip_str in resolved_ips:
# VULNERABLE: Direct string comparison of a resolved IP against an IPv4 blocklist
# can be bypassed if the IP is represented in an IPv6 transition format.
if ip_str in CLOUD_METADATA_BLOCKLIST:
return True
return False
except (ValueError, socket.gaierror):
# In case of resolution or parsing errors, deny access
return TruePatched code sample
from urllib.parse import urlparse
import socket
import ipaddress
# Simplified representation of the cloud-metadata blocklist as IP objects
CLOUD_METADATA_BLOCKLIST = {ipaddress.IPv4Address("169.254.169.254")}
def _is_url_blocked(url: str) -> bool:
"""
Checks if a URL points to a blocked resource, like a cloud metadata endpoint.
This check is intended to run when force_download='allow-local' is used.
"""
try:
hostname = urlparse(url).hostname
if not hostname:
return False
# Resolve hostname to its IP addresses to handle various formats
addr_infos = socket.getaddrinfo(hostname, None)
resolved_ips = {info[4][0] for info in addr_infos}
for ip_str in resolved_ips:
ip = ipaddress.ip_address(ip_str)
# Normalize IPv4-mapped IPv6 addresses to their IPv4 form
if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped:
ip = ip.ipv4_mapped
# FIX: Check the resolved and normalized IP address object against a blocklist
# of IP objects, preventing bypasses via alternative encodings.
if ip in CLOUD_METADATA_BLOCKLIST:
return True
return False
except (ValueError, socket.gaierror):
# In case of resolution or parsing errors, deny access
return TruePayload
http://[::ffff:169.254.169.254]/latest/meta-data/iam/security-credentials/
Cite this entry
@misc{vaitp:cve202646678,
title = {{Pydantic AI: SSRF via IPv6-encoded IPs exposes cloud credentials.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-46678},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-46678/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
