Pydantic AI has Server-Side Request Forgery (SSRF) in URL Download Handling
GHSA-2jrp-274c-jhv3 · CVE-2026-25580 · PYSEC-2026-2976 · PYSEC-2026-2980 · 갱신 2026. 7. 14.
영향 받는 패키지
| 생태계 | 패키지 | 수정 버전 |
|---|---|---|
| PyPI | pydantic-ai | 1.56.0 |
| PyPI | pydantic-ai-slim | 1.56.0 |
GHSA-2jrp-274c-jhv3 · CVE-2026-25580 · PYSEC-2026-2976 · PYSEC-2026-2980 · 갱신 2026. 7. 14.
| 생태계 | 패키지 | 수정 버전 |
|---|---|---|
| PyPI | pydantic-ai | 1.56.0 |
| PyPI | pydantic-ai-slim | 1.56.0 |
Agent.to_webclai webUses VercelAIAdapter - Chat interfaces built with Vercel AI SDK allow users to submit messages containing URLs that are processed server-side.
Uses AGUIAdapter or Agent.to_ag_ui - The AG-UI protocol allows users to provide file references with URLs as part of agent interactions.
Exposes a custom API accepting message history - Any endpoint that accepts message history or ImageUrl, AudioUrl, VideoUrl, DocumentUrl objects from user input.
Via chat interface, an attacker submits a message with a file attachment pointing to an internal resource:
{
"role": "user",
"parts": [
{"type": "file", "mediaType": "image/png", "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}
]
}
Multiple model integrations download URL content in certain conditions:
| Provider | Downloaded Types |
|---|---|
OpenAIChatModel | AudioUrl, DocumentUrl |
AnthropicModel | DocumentUrl (text/plain) |
GoogleModel (GLA) | All URL types (except YouTube and Files API URLs) |
XaiModel | DocumentUrl |
BedrockConverseModel | ImageUrl, DocumentUrl, VideoUrl (non-S3 URLs) |
OpenRouterModel | AudioUrl |
Upgrade to the patched version or later. The fix adds comprehensive SSRF protection:
allow-local)http:// and https:// protocolsforce_download='allow-local' OptionIf an application legitimately needs to access local/private network resources (e.g., in a fully trusted internal environment), it can explicitly opt in:
from pydantic_ai import ImageUrl
# Default behavior: private IPs are blocked
ImageUrl(url="http://internal-service/image.png") # Raises ValueError
# Opt-in to allow local access (use with caution)
ImageUrl(url="http://internal-service/image.png", force_download='allow-local')
Important: Cloud metadata endpoints (169.254.169.254, fd00:ec2::254, 100.100.100.200) are always blocked, even with allow-local.
If a project cannot upgrade immediately, use a history processor to filter out URLs targeting local/private addresses:
import ipaddress
import socket
from urllib.parse import urlparse
from pydantic_ai import Agent, ModelMessage, ModelRequest
from pydantic_ai.messages import AudioUrl, DocumentUrl, ImageUrl, VideoUrl
def is_private_url(url: str) -> bool:
"""Check if a URL targets a private/internal IP address."""
try:
parsed = urlparse(url)
hostname = parsed.hostname
if not hostname:
return True # Invalid URL, block it
# Resolve hostname to IP
ip_str = socket.gethostbyname(hostname)
ip = ipaddress.ip_address(ip_str)
# Block private, loopback, and link-local addresses
return ip.is_private or ip.is_loopback or ip.is_link_local
except (socket.gaierror, ValueError):
return True # DNS resolution failed, block it
def filter_private_urls(messages: list[ModelMessage]) -> list[ModelMessage]:
"""Remove URL parts that target private/internal addresses."""
url_types = (ImageUrl, AudioUrl, VideoUrl, DocumentUrl)
filtered = []
for msg in messages:
if isinstance(msg, ModelRequest):
safe_parts = [
part for part in msg.parts
if not (isinstance(part, url_types) and is_private_url(part.url))
]
if safe_parts:
filtered.append(ModelRequest(parts=safe_parts))
else:
filtered.append(msg)
return filtered
# Apply the filter to your agent
agent = Agent('openai:gpt-5', history_processors=[filter_private_urls])
The fix introduces a new _ssrf.py module with comprehensive protection:
http:// and https:// allowed127.0.0.0/8, ::1/128 (loopback)10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 (private)169.254.0.0/16, fe80::/10 (link-local)100.64.0.0/10 (CGNAT)fc00::/7 (unique local)2002::/16 (6to4, can embed private IPv4)169.254.169.254, fd00:ec2::254, 100.100.100.200