Skip to content

SSRF Bypass via Carrier-Grade NAT (100.64.0.0/10) on Python 3.11+ #16

Description

@spartan8806

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_privateTrue (safe)
  • Python 3.11+: ipaddress.ip_address('100.64.0.1').is_privateFalse (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

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions