mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-08-09 06:58:41 -04:00
fix(url-safety): reject RFC 6598 shared address space in strict mode (#5474)
* security(url-safety): reject RFC 6598 shared address space in strict mode Strict mode (block_private=True) is a full SSRF lockdown, but it only rejected is_private and is_loopback targets. CPython does not classify RFC 6598 shared/CGNAT space (100.64.0.0/10) as is_private (it is "shared", not "private"), so a public redirect into 100.64.0.1 passed the per-hop guard and still issued the request to a potentially internal CGNAT service. not is_global would also exclude it, but only on CPython 3.11.10+/3.12.4+/ 3.13+; the CI matrix runs 3.11/3.12, so reject the range explicitly to stay correct across patch levels and the 3.14 runtime image. Default local-first mode is unchanged. Adds strict-mode coverage for shared, non-global, and public targets. * docs(url-safety): correct CGNAT is_global rationale in strict-mode comment The prior comment claimed `not is_global` catches 100.64.0.0/10 only on CPython 3.11.10+/3.12.4+/3.13+. That is inaccurate for CGNAT: is_global is False for 100.64.0.1 on every supported version (verified 3.10-3.14). The version-fragility applies to other ranges gh-113171 touched, not CGNAT. The explicit range reject is still the right choice; restate the reason as is_private not covering shared space, and not coupling strict mode to is_global's broader, cross-version definition. No behavior change.
This commit is contained in:
+14
-2
@@ -25,6 +25,14 @@ from urllib.parse import urlparse
|
||||
|
||||
ALLOWED_SCHEMES = ("http", "https")
|
||||
|
||||
# RFC 6598 shared address space (carrier-grade NAT). It is not globally
|
||||
# routable, but CPython does not classify it as ``is_private`` (it is "shared",
|
||||
# not "private"), so the is_private/is_loopback checks miss it. Reject the range
|
||||
# explicitly. This closes exactly the shared-space gap without coupling strict
|
||||
# mode to ``is_global``'s broader definition, which has shifted across CPython
|
||||
# versions for other special ranges.
|
||||
_SHARED_ADDRESS_SPACE_V4 = ipaddress.ip_network("100.64.0.0/10")
|
||||
|
||||
|
||||
def _default_resolver(host: str) -> List[str]:
|
||||
"""Resolve a hostname to the list of IP strings it maps to (A + AAAA)."""
|
||||
@@ -40,8 +48,12 @@ def _classify(ip: ipaddress._BaseAddress, *, block_private: bool) -> Optional[st
|
||||
return f"link-local address blocked (SSRF metadata risk): {ip}"
|
||||
if ip.is_multicast or ip.is_reserved or ip.is_unspecified:
|
||||
return f"disallowed address: {ip}"
|
||||
if block_private and (ip.is_private or ip.is_loopback):
|
||||
return f"private/loopback address blocked: {ip}"
|
||||
if block_private and (
|
||||
ip.is_private
|
||||
or ip.is_loopback
|
||||
or (isinstance(ip, ipaddress.IPv4Address) and ip in _SHARED_ADDRESS_SPACE_V4)
|
||||
):
|
||||
return f"private/shared/loopback address blocked: {ip}"
|
||||
return None
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user