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.
- CWE
- CWE-918
- 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 ::
