Security Vulnerability Report
Severity: MEDIUM (CWE-918: Server-Side Request Forgery)
Reporter: Conner Webber (conner.webber000@gmail.com)
Affected: All versions of safehttpx on Python 3.11+
Note: I attempted to use GitHub's Private Vulnerability Reporting for this repo, but it is not enabled. I'm filing this as an issue since there is no other responsible disclosure channel listed. I recommend enabling PVRA at Settings > Security > Private vulnerability reporting.
Summary
The is_public_ip() function in safehttpx/__init__.py (lines 18-29) fails to block the Carrier-Grade NAT (CGN) range 100.64.0.0/10 (RFC 6598) on Python 3.11+. This allows an attacker to bypass SSRF protections and reach internal infrastructure accessible via CGN addresses.
safehttpx is the SSRF protection layer for Gradio (5M+ downloads/month), making this a supply-chain impact issue.
Root Cause
is_public_ip() relies on five checks from Python's ipaddress module:
ip_obj.is_private
ip_obj.is_loopback
ip_obj.is_link_local
ip_obj.is_multicast
ip_obj.is_reserved
In Python 3.11+, the semantics of is_private were tightened per CPython #86545. The CGN range 100.64.0.0/10 is classified as "shared address space" (RFC 6598), not "private" (RFC 1918). As a result:
- Python 3.10:
ipaddress.ip_address('100.64.0.1').is_private → True (safe)
- Python 3.11+:
ipaddress.ip_address('100.64.0.1').is_private → False (BYPASS)
The entire range 100.64.0.0 - 100.127.255.255 (4M+ addresses) passes ALL five checks on Python 3.11+.
Attack Scenario
- Attacker registers a domain (e.g.,
evil.attacker.com) with a DNS A record pointing to 100.64.0.1
- Attacker submits this URL to a Gradio application that fetches remote resources via safehttpx
- safehttpx resolves the domain, checks the IP via
is_public_ip() — all five checks pass on Python 3.11+
- The request is sent to
100.64.0.1, reaching internal CGN infrastructure
- In cloud environments (AWS, GCP, Azure), CGN ranges may route to internal metadata services or other tenants' infrastructure
Proof of Concept
import ipaddress
import sys
print(f"Python {sys.version}")
ip = ipaddress.ip_address('100.64.0.1')
print(f"is_private: {ip.is_private}") # False on 3.11+
print(f"is_loopback: {ip.is_loopback}") # False
print(f"is_link_local: {ip.is_link_local}") # False
print(f"is_multicast: {ip.is_multicast}") # False
print(f"is_reserved: {ip.is_reserved}") # False
print(f"is_global: {ip.is_global}") # False (correct!)
print(f"\nAll safehttpx checks pass: {not any([ip.is_private, ip.is_loopback, ip.is_link_local, ip.is_multicast, ip.is_reserved])}")
print(f"But is_global correctly blocks: {ip.is_global}")
Suggested Fix
Option A (targeted): Add explicit CGN range check:
CGN_NETWORK = ipaddress.ip_network('100.64.0.0/10')
def is_public_ip(ip: str) -> bool:
ip_obj = ipaddress.ip_address(ip)
if ip_obj in CGN_NETWORK:
return False
# ... existing checks ...
Option B (comprehensive, recommended): Replace all negative checks with is_global:
def is_public_ip(ip: str) -> bool:
ip_obj = ipaddress.ip_address(ip)
return ip_obj.is_global
is_global returns False for CGN addresses across all Python versions (including 3.14), and also covers all other non-routable ranges. This is a one-line fix that is more robust than maintaining an explicit blocklist.
What Is NOT Vulnerable (Positive Notes)
- DNS rebinding: Mitigated via IP pinning in
AsyncSecureTransport ✅
- Redirect-based SSRF:
follow_redirects=False is hardcoded ✅
- IPv6 private addresses: Correctly caught by existing checks ✅
- Decimal/hex IP obfuscation: Rejected by
ipaddress module's strict parsing ✅
Impact
- Any Gradio application running on Python 3.11+ that uses safehttpx to fetch user-supplied URLs is vulnerable to SSRF via CGN addresses
- Cloud environments where CGN ranges route to internal services are at highest risk
- Affects the Gradio ecosystem (5M+ downloads/month)
References
Security Vulnerability Report
Severity: MEDIUM (CWE-918: Server-Side Request Forgery)
Reporter: Conner Webber (conner.webber000@gmail.com)
Affected: All versions of safehttpx on Python 3.11+
Summary
The
is_public_ip()function insafehttpx/__init__.py(lines 18-29) fails to block the Carrier-Grade NAT (CGN) range100.64.0.0/10(RFC 6598) on Python 3.11+. This allows an attacker to bypass SSRF protections and reach internal infrastructure accessible via CGN addresses.safehttpx is the SSRF protection layer for Gradio (5M+ downloads/month), making this a supply-chain impact issue.
Root Cause
is_public_ip()relies on five checks from Python'sipaddressmodule:In Python 3.11+, the semantics of
is_privatewere tightened per CPython #86545. The CGN range100.64.0.0/10is classified as "shared address space" (RFC 6598), not "private" (RFC 1918). As a result:ipaddress.ip_address('100.64.0.1').is_private→True(safe)ipaddress.ip_address('100.64.0.1').is_private→False(BYPASS)The entire range
100.64.0.0 - 100.127.255.255(4M+ addresses) passes ALL five checks on Python 3.11+.Attack Scenario
evil.attacker.com) with a DNS A record pointing to100.64.0.1is_public_ip()— all five checks pass on Python 3.11+100.64.0.1, reaching internal CGN infrastructureProof of Concept
Suggested Fix
Option A (targeted): Add explicit CGN range check:
Option B (comprehensive, recommended): Replace all negative checks with
is_global:is_globalreturnsFalsefor CGN addresses across all Python versions (including 3.14), and also covers all other non-routable ranges. This is a one-line fix that is more robust than maintaining an explicit blocklist.What Is NOT Vulnerable (Positive Notes)
AsyncSecureTransport✅follow_redirects=Falseis hardcoded ✅ipaddressmodule's strict parsing ✅Impact
References