VAITP Dataset

← Back to the dataset

CVE-2025-54381

BentoML is vulnerable to unauthenticated SSRF via its file upload feature.

  • CVSS 9.9
  • CWE-918
  • Design Defects
  • Remote

BentoML is a Python library for building online serving systems optimized for AI apps and model inference. In versions 1.4.0 until 1.4.19, the file upload processing system contains an SSRF vulnerability that allows unauthenticated remote attackers to force the server to make arbitrary HTTP requests. The vulnerability stems from the multipart form data and JSON request handlers, which automatically download files from user-provided URLs without validating whether those URLs point to internal network addresses, cloud metadata endpoints, or other restricted resources. The documentation explicitly promotes this URL-based file upload feature, making it an intended design that exposes all deployed services to SSRF attacks by default. Version 1.4.19 contains a patch for the issue.

CVSS base score
9.9
Published
2025-07-29
OWASP
A10 Server-Side Request Forgery
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Design Defects
Subcategory
Server-Side Request Forgery (SSRF)
Accessibility scope
Remote
Impact
Unauthorized Access
Affected component
BentoML
Fixed by upgrading
Yes

Solution

Upgrade BentoML to version 1.4.19 or later.

Vulnerable code sample

import requests
from flask import Flask, request, jsonify

# This is a conceptual example using Flask to represent the vulnerability.
# The actual BentoML implementation would be different but would contain
# similar unsafe logic for handling file uploads from URLs.

app = Flask(__name__)

# A simulated service endpoint that is vulnerable to SSRF.
# It accepts a JSON payload with a 'file_url' key pointing to a remote file.
@app.route('/process_file', methods=['POST'])
def process_file_from_url():
    """
    This endpoint simulates the vulnerable behavior described in the CVE.
    It fetches a file from a user-supplied URL without any validation.
    An attacker can point 'file_url' to internal network resources.
    """
    if not request.is_json:
        return jsonify({"error": "Request body must be JSON"}), 400

    data = request.get_json()
    file_url = data.get('file_url')

    if not file_url:
        return jsonify({"error": "The 'file_url' field is required"}), 400

    try:
        # VULNERABLE CODE: The server makes a direct HTTP request to the URL
        # provided by the user without validating if it is an internal or
        # restricted address. This is the core of the SSRF vulnerability.
        response = requests.get(file_url, timeout=5)
        response.raise_for_status()  # Raise an exception for HTTP errors

        # Simulate processing the file's content
        file_content = response.content
        processing_result = {
            "status": "success",
            "source_url": file_url,
            "file_size_bytes": len(file_content)
        }
        return jsonify(processing_result), 200

    except requests.exceptions.RequestException as e:
        # Error messages can leak information about the internal network,
        # such as connection refused, timeouts, or DNS resolution failures.
        return jsonify({
            "status": "error",
            "message": f"Failed to fetch file from URL: {str(e)}"
        }), 500

# To run this example:
# 1. Install Flask and requests: pip install Flask requests
# 2. Save the code as `vulnerable_app.py` and run: python vulnerable_app.py
#
# To exploit the vulnerability, send a POST request pointing to an internal service:
#
# # Example: Probing a local service running on port 8000
# curl -X POST -H "Content-Type: application/json" \
#      -d '{"file_url": "http://127.0.0.1:8000"}' \
#      http://127.0.0.1:5000/process_file
#
# # Example: Attempting to access cloud metadata (on AWS/GCP/Azure)
# curl -X POST -H "Content-Type: application/json" \
#      -d '{"file_url": "http://169.254.169.254/latest/meta-data/"}' \
#      http://127.0.0.1:5000/process_file

if __name__ == '__main__':
    # WARNING: This code is intentionally vulnerable for demonstration purposes.
    # Do not use it in a production environment.
    app.run(host='0.0.0.0', port=5000)

Patched code sample

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

def is_safe_url(url: str) -> bool:
    """
    Validates a URL to prevent SSRF attacks.

    This function represents the core of the fix. It ensures that the URL:
    1. Uses an allowed scheme (http, https).
    2. Resolves to a public IP address, not a private, reserved, loopback,
       or link-local address.

    Args:
        url: The URL to validate.

    Returns:
        True if the URL is safe, False otherwise.
    """
    try:
        parsed_url = urlparse(url)

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

        # 2. Hostname validation: Ensure a hostname is present
        hostname = parsed_url.hostname
        if not hostname:
            return False

        # 3. IP address resolution and validation
        # Use socket.getaddrinfo for robust resolution (supports IPv4/IPv6)
        addr_info = socket.getaddrinfo(hostname, parsed_url.port or 0)
        
        # In case of multiple IPs (e.g., DNS round-robin), check all of them
        for family, socktype, proto, canonname, sockaddr in addr_info:
            ip_str = sockaddr[0]
            ip = ipaddress.ip_address(ip_str)

            # The crucial security check: block non-public IPs
            if not ip.is_global:
                # is_global is a convenience method that checks if the address is not
                # private, reserved, loopback, or link-local. This is the main
                # defense against SSRF attacks targeting internal resources.
                return False

        # If all resolved IPs are public/global, the URL is considered safe
        return True

    except (ValueError, socket.gaierror):
        # ValueError for invalid URL, gaierror for DNS resolution failure
        return False


def patched_file_downloader(url: str) -> bytes:
    """
    Represents the fixed file download functionality in BentoML.

    It first calls is_safe_url to validate the target URL before making
    any network request. This prevents the server from being forced to
    request resources from internal or restricted network locations.
    """
    # This is the security control point. The vulnerable version would
    # lack this check and proceed directly to the requests.get() call.
    if not is_safe_url(url):
        raise ValueError(f"URL validation failed. Access to '{url}' is blocked for security reasons.")

    print(f"[INFO] URL '{url}' passed validation. Proceeding with download.")
    try:
        # A timeout is crucial for any production-grade network request
        with requests.get(url, stream=True, timeout=10) as response:
            response.raise_for_status()
            # For demonstration, we'll return the content.
            # A real implementation might stream the file to disk.
            return response.content
    except requests.exceptions.RequestException as e:
        raise IOError(f"Failed to fetch file from URL: {e}") from e

# --- Demonstration of the fix ---
if __name__ == "__main__":
    # A list of URLs to test the fix against.
    test_urls = {
        # Potentially vulnerable URLs that should be blocked
        "http://127.0.0.1/server-info": False,  # Loopback (IPv4)
        "http://[::1]/admin": False,  # Loopback (IPv6)
        "http://localhost/secrets.txt": False, # Resolves to loopback
        "http://169.254.169.254/latest/meta-data/": False,  # AWS/GCP Metadata Service
        "http://192.168.1.1/router_status.html": False,  # Private network
        "http://10.0.0.5/internal_api": False, # Private network
        "file:///etc/passwd": False, # Disallowed scheme
        "ftp://example.com/data": False, # Disallowed scheme

        # Safe, public URLs that should be allowed
        "https://www.bentoml.com/": True,
        "http://example.com/": True,
    }

    print("--- Testing the Patched Downloader with SSRF Prevention ---")
    for url, should_succeed in test_urls.items():
        print(f"\nAttempting to fetch: {url}")
        try:
            patched_file_downloader(url)
            if should_succeed:
                print(f"[SUCCESS] Correctly allowed safe URL.")
            else:
                print(f"[FAILURE] INCORRECTLY allowed a malicious URL.")
        except (ValueError, IOError) as e:
            if not should_succeed:
                print(f"[SUCCESS] Correctly blocked malicious URL. Reason: {e}")
            else:
                print(f"[FAILURE] INCORRECTLY blocked a safe URL. Reason: {e}")

Payload

{
  "input_file": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
}

Cite this entry

@misc{vaitp:cve202554381,
  title        = {{BentoML is vulnerable to unauthenticated SSRF via its file upload feature.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2025},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-54381},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-54381/}}
}
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 ::