A two-layer SSRF egress guard for Python's stdlib HTTP client that survives DNS rebinding.
Project description
pantheon-ssrf-guard
A small, dependency-free SSRF egress guard for Python's standard-library HTTP client — one that survives DNS rebinding.
The guarantee: an outbound request cannot reach a private, loopback, link-local, cloud-metadata, or CGN address — even if the hostname resolves public at check time and rebinds to an internal IP before the socket connects.
Extracted from PANTHEON, a multi-tenant AI substrate, where it is the single canonical guard on every outbound path (link imports, uploaded-URL fetches, external MCP-server transports).
Why a pre-check isn't enough
The common "block internal hosts" approach resolves the hostname and rejects private IPs before connecting. An attacker defeats it with DNS rebinding: the name resolves to a public IP when you check it, then to 127.0.0.1 / 169.254.169.254 (cloud metadata) / an internal service by the time the socket actually opens.
pantheon-ssrf-guard uses two layers:
host_is_public(host)— the fast pre-check: reject if any resolved address is non-public.GuardedHTTP(S)Connection— at connect time, re-check the IP the socket actually reached. This is the layer a pre-check can't provide, and it's what closes the rebind window.
The classifier's catch-all is not ip.is_global, so it also rejects everything the obvious flags miss — RFC 6598 shared/CGN space (100.64.0.0/10, used by Kubernetes/internal load balancers and the Alibaba/Tencent/Oracle metadata endpoint 100.100.100.200), plus benchmark, documentation, and NAT64 ranges.
Install
pip install pantheon-ssrf-guard # or: copy the single ssrf_guard.py file into your project
Use
from ssrf_guard import guarded_opener, SsrfBlocked
opener = guarded_opener() # https-capable; redirects refused by default (each hop re-checked if allowed)
try:
with opener.open("https://example.com/data.json", timeout=10) as resp:
body = resp.read()
except SsrfBlocked:
... # the target resolved (or rebound) to a non-public address — refused before any data was sent
Lower-level pieces are exported too if you're building your own client:
from ssrf_guard import host_is_public, ip_is_public, peer_is_public
from ssrf_guard import GuardedHTTPHandler, GuardedHTTPSHandler
What it does not do
- It doesn't proxy, cache, or rate-limit — it only refuses non-public destinations.
- It guards
urllib/http.client. Forhttpx/requestsyou'd apply the same idea at their transport layer (the classification functions here are reusable). - It is not a WAF; it's the one specific control that stops server-side request forgery to internal infrastructure.
Changelog
- 0.1.1 — the pre-check is now wired into the opener (so "two-layer" is true, not aspirational): a
GuardedHTTP(S)Connectionrejects a non-public host by name BEFORE opening the socket, then re-checks the actual peer at connect time (the rebind guard). And IPv4-mapped (::ffff:…) + NAT64 (64:ff9b::/96) addresses are normalised to their embedded v4 before classifying, so an internal target smuggled inside a v6 address is blocked version-independently instead of trusting whicheveris_*flags a given CPython sets. - 0.1.0 — initial release.
License
Apache-2.0. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pantheon_ssrf_guard-0.1.1.tar.gz.
File metadata
- Download URL: pantheon_ssrf_guard-0.1.1.tar.gz
- Upload date:
- Size: 10.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aa7423d7c763297cc067a78b1e4f17bcf41f8b13638d7ab17adc630cf5b4189d
|
|
| MD5 |
3781ffe267307cc6e0b0361674b74aa6
|
|
| BLAKE2b-256 |
af38162f0e96a167f76f55876810d7232180996b600b59518385beba9afae2ae
|
Provenance
The following attestation bundles were made for pantheon_ssrf_guard-0.1.1.tar.gz:
Publisher:
publish.yml on Igfray/pantheon-ssrf-guard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pantheon_ssrf_guard-0.1.1.tar.gz -
Subject digest:
aa7423d7c763297cc067a78b1e4f17bcf41f8b13638d7ab17adc630cf5b4189d - Sigstore transparency entry: 2163729256
- Sigstore integration time:
-
Permalink:
Igfray/pantheon-ssrf-guard@296821d9b9f0ba329e7ef0273e9cefab8b6a4a28 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/Igfray
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@296821d9b9f0ba329e7ef0273e9cefab8b6a4a28 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pantheon_ssrf_guard-0.1.1-py3-none-any.whl.
File metadata
- Download URL: pantheon_ssrf_guard-0.1.1-py3-none-any.whl
- Upload date:
- Size: 9.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0f33effa39c50609786543868d7989a4b52d9e2cbc93da6534f8c88501779649
|
|
| MD5 |
21e868826e23f5284f69c2cb14c1ab06
|
|
| BLAKE2b-256 |
18f977c82fc8f3292a2767d6de5d3961acf45218d7b8fc51eed7a3bd1b6033ce
|
Provenance
The following attestation bundles were made for pantheon_ssrf_guard-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on Igfray/pantheon-ssrf-guard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pantheon_ssrf_guard-0.1.1-py3-none-any.whl -
Subject digest:
0f33effa39c50609786543868d7989a4b52d9e2cbc93da6534f8c88501779649 - Sigstore transparency entry: 2163729260
- Sigstore integration time:
-
Permalink:
Igfray/pantheon-ssrf-guard@296821d9b9f0ba329e7ef0273e9cefab8b6a4a28 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/Igfray
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@296821d9b9f0ba329e7ef0273e9cefab8b6a4a28 -
Trigger Event:
push
-
Statement type: