Repository navigation
fix(security): require is_global in the SSRF guard (GHSA-cx77-j6h8-3382) - #165
Merged
Merged
Conversation
validate_public_url classified resolved addresses with a category denylist, and 100.64.0.0/10 (RFC 6598 shared address space) satisfies none of those categories: it is is_global False but neither private nor reserved. A URL resolving into it passed the guard and was fetched server-side, and the redirect hook allowed hops that stayed inside the range. That range hosts internal services on GKE and other Kubernetes clusters, cloud internal endpoints and ISP CGNAT. The guard now tests positively for is_global, keeps the category checks alongside it, and names the non-public networks explicitly because is_global's backing table has shifted across CPython patch releases. IPv4-mapped addresses are still unwrapped first. Bumps to 1.0.125 and documents the advisory in SECURITY.md.
PM-Labs
pushed a commit
to pmlabs-org/mcp-meta-ads
that referenced
this pull request
Oct 11, 2026
…) (pipeboard-co#165) validate_public_url classified resolved addresses with a category denylist, and 100.64.0.0/10 (RFC 6598 shared address space) satisfies none of those categories: it is is_global False but neither private nor reserved. A URL resolving into it passed the guard and was fetched server-side, and the redirect hook allowed hops that stayed inside the range. That range hosts internal services on GKE and other Kubernetes clusters, cloud internal endpoints and ISP CGNAT. The guard now tests positively for is_global, keeps the category checks alongside it, and names the non-public networks explicitly because is_global's backing table has shifted across CPython patch releases. IPv4-mapped addresses are still unwrapped first. Bumps to 1.0.125 and documents the advisory in SECURITY.md.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes GHSA-cx77-j6h8-3382, reported by @Gal3m and @mohammad228. Follow-up to the SSRF guard shipped in 1.0.115 (GHSA-45gf-fjxp-cjpq).
The gap
validate_public_urlclassified a resolved address with a category denylist —is_private or is_loopback or is_link_local or is_reserved or is_multicast or is_unspecified.100.64.0.0/10(RFC 6598 shared address space) satisfies none of them:So a URL resolving into that range passed the guard and was fetched server-side, and the redirect re-validation hook allowed hops that stayed inside it. That range is not exotic — GKE and other Kubernetes clusters default pod/service CIDRs into it, as do cloud internal endpoints and ISP CGNAT. Loopback, RFC 1918 and link-local (including cloud metadata) were always blocked, so this was a partial internal-range SSRF confined to shared address space.
The fix
_ip_is_disallowednow tests positively forip.is_globaland rejects everything else, so non-public ranges no longer have to be enumerated to be caught. Two things are kept alongside it rather than replaced:_BLOCKED_NETWORKSlist (100.64.0.0/10,192.0.0.0/24,198.18.0.0/15, the RFC 5737 TEST-NETs,240.0.0.0/4,2001:db8::/32,64:ff9b:1::/48) —is_global's backing table has shifted across CPython patch releases, and this guard should not depend on which patch release the operator runs.IPv4-mapped IPv6 addresses are still unwrapped first, so
::ffff:100.64.1.1is rejected too.Verified against the advisory's PoC cases — all now blocked, public literals still allowed:
Tests
tests/test_ssrf_url_validation.pygains 10 blocked URLs (the shared range, its IPv4-mapped form, protocol assignments, benchmarking, all three TEST-NETs, class E, IPv6 documentation) plus:100.64.x.xisis_private == Falsebefore asserting it is blocked, so it cannot pass for the wrong reason;100.63.255.255and100.128.0.0stay reachable — the range is not all of100.0.0.0/8;100.64.1.1is stopped at the hop.Full suite: 576 passed, 9 skipped. Bumped to 1.0.125 with a SECURITY.md entry.