gradio-app / gradio-app/safehttpx
SSRF Bypass via Carrier-Grade NAT (100.64.0.0/10) on Python 3.11+
- Dominant language
- Python
- Stars
- 72
- Forks
- 8
- PR merge metrics
- No merged PRs in 30d
Description
## 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:
```python
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](https://github.com/python/cpython/issues/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
1. Attacker registers a domain (e.g., `evil.attacker.com`) with a DNS A record pointing to `100.64.0.1`
2. Attacker submits this URL to a Gradio application that fetches remote resources via safehttpx
3. safehttpx resolves the domain, checks the IP via `is_public_ip()` — all five checks pass on Python 3.11+
4. The request is sent to `100.64.0.1`, reaching internal CGN infrastructure
5. In cloud environments (AWS, GCP, Azure), CGN ranges may route to internal metadata services or other tenants' infrastructure
## Proof of Concept
```python
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:
```python
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`:
```python
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
- [RFC 6598: IANA-Reserved IPv4 Prefix for Shared Address Space](https://www.rfc-editor.org/rfc/rfc6598)
- [CPython #86545: is_private semantics change](https://github.com/python/cpython/issues/86545)
- [CWE-918: Server-Side Request Forgery](https://cwe.mitre.org/data/definitions/918.html)
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.