#5644·BentoML

SSRF blocklist bypass via CGNAT range (100.64.0.0/10)

Author: geo-chenCreated Jun 30, 2026Updated Aug 22, 2026
Labelsbug

Describe the bug

reported via email on 2 June 2026 - no responses since.

I am reporting an incomplete SSRF fix in BentoML that allows unauthenticated network attackers to make the server issue outbound HTTP requests to CGNAT-range addresses (RFC 6598, 100.64.0.0/10), which the existing blocklist in make_safe_connect() does not cover.

Background: CVE-2025-54381 (GHSA-mrmq-3q62-6cc8) patched SSRF in file upload processing by adding make_safe_connect() to reject connections to private, loopback, and link-local IPs. The patch is correct for those ranges but does not include the CGNAT range 100.64.0.0/10.

Root cause: Python's ipaddress module does not classify 100.64.0.0/10 as private, loopback, or link-local, so the check in uri.py lines 88-95 passes these addresses unchallenged.

Affected code:

src/bentoml/_internal/utils/uri.py -- make_safe_connect(), lines 88-95: if ip.is_private or ip.is_loopback or ip.is_link_local: raise socket.gaierror(f"Blocked private IP address {host}")

This condition evaluates to False for 100.64.1.1 (CGNAT), allowing the connection.

Trigger paths: src/_bentoml_impl/serde.py -- MultipartSerde.ensure_file(), lines 206-210 src/_bentoml_impl/serde.py -- JSONSerde.parse_request(), lines 176-183

PoC (tested on commit 71f55b5e, bentoml 1.4.39, Linux/uvloop):

Service with PIL.Image.Image input:

curl -X POST http://localhost:3000/classify -F "img=http://100.64.1.1:80/internal"

Result: connection attempted (no "blocked" error), timeout after 5 seconds -- confirmed in server log as "httpx.ConnectTimeout" rather than the expected "Connection blocked due to insecure input URL".

For comparison, a private IP is correctly blocked:

curl -X POST http://localhost:3000/classify -F "img=http://192.168.0.1/internal" Response: {"error":"Connection blocked due to insecure input URL"}

Suggested fix: Extend the blocked condition to include is_reserved and to explicitly check for CGNAT:

BLOCKED_NETWORKS = [ipaddress.ip_network("100.64.0.0/10")] # RFC 6598 CGNAT if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved or any(ip in n for n in BLOCKED_NETWORKS): raise socket.gaierror(...)

Impact: Allows unauthenticated attackers to probe and potentially read from internal services reachable at CGNAT addresses. In cloud environments (AWS, GCP, ISP-managed infrastructure) where CGNAT IPs are used for internal load balancers or service endpoints, this enables meaningful SSRF exploitation.

Affected versions: all versions through 1.4.39 (latest).

To reproduce

No response

Expected behavior

No response

Environment

1.4.39 on docker