Metadata-Version: 2.4
Name: pantheon-ssrf-guard
Version: 0.1.0
Summary: A two-layer SSRF egress guard for Python's stdlib HTTP client that survives DNS rebinding.
Project-URL: Homepage, https://github.com/Igfray/pantheon-ssrf-guard
Author: Isaac Teague Frayling
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: dns-rebinding,egress,http,security,ssrf,urllib
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Security
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# pantheon-ssrf-guard

[![tests](https://github.com/Igfray/pantheon-ssrf-guard/actions/workflows/ci.yml/badge.svg)](https://github.com/Igfray/pantheon-ssrf-guard/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/pantheon-ssrf-guard.svg)](https://pypi.org/project/pantheon-ssrf-guard/)
[![Python](https://img.shields.io/pypi/pyversions/pantheon-ssrf-guard.svg)](https://pypi.org/project/pantheon-ssrf-guard/)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)

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](https://pantheonlabs.co.uk), 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**:

1. **`host_is_public(host)`** — the fast pre-check: reject if *any* resolved address is non-public.
2. **`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

```bash
pip install pantheon-ssrf-guard   # or: copy the single ssrf_guard.py file into your project
```

## Use

```python
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:

```python
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`. For `httpx`/`requests` you'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.

## License

Apache-2.0. See `LICENSE`.
