SSRF- and DNS-rebinding-safe outbound HTTP client: an egress host allowlist plus a DNS-pinned httpx transport.
Project description
egressweave
SSRF- and DNS-rebinding-safe outbound HTTP for Python.
egressweave validates an outbound URL against an explicit host allowlist,
refuses any target that resolves to a non-globally-routable address, and hands
back an httpx.AsyncClient whose every connection is pinned to the validated
addresses — rejecting any host or port that changes after validation.
It exists because the naive pattern — resolve, check the IP, then
httpx.get(url) — is unsafe: the attacker-controlled DNS answer can change
between the check and the connect (a TOCTOU / DNS-rebinding attack, CWE-350),
and a permissive URL parser can be tricked into reaching internal services
(SSRF, CWE-918).
What it defends against
- SSRF (CWE-918): rejects private, loopback, link-local, reserved,
multicast, unspecified, and otherwise non-global addresses; rejects embedded
credentials, query/fragment, plaintext
httpto remote hosts, IP-literal hosts, backslash smuggling, and ASCII control characters. - DNS rebinding / validate-then-connect TOCTOU (CWE-350): resolves all addresses up front, validates each, and pins them into a custom transport that re-validates on every connect and refuses any host/port drift.
- Egress allowlist: only hostnames you explicitly list are reachable; wildcards are refused — the allowlist is exact.
- Redirects are disabled and environment proxies ignored (
trust_env=False), so a302cannot bounce a request to an unvalidated host, and Unix sockets are refused.
Install
pip install egressweave
Quickstart
from egressweave import EgressPolicy, build_egress_http_client
policy = EgressPolicy.from_hosts("api.openai.com, api.anthropic.com")
normalized_url, client = await build_egress_http_client(
"https://api.openai.com/v1", policy=policy
)
async with client:
resp = await client.get(f"{normalized_url}/models")
Validate without building a client:
from egressweave import EgressPolicy, validate_egress_url, EgressNotAllowedError
policy = EgressPolicy.from_hosts("api.openai.com")
try:
url = validate_egress_url("https://api.openai.com/v1", policy=policy)
except EgressNotAllowedError:
... # generic, non-leaking rejection
Local development (Ollama-style container name that resolves to a private IP):
policy = EgressPolicy.from_hosts("ollama", allow_local=True)
API
| Symbol | Purpose |
|---|---|
EgressPolicy |
Injected allowlist config: from_hosts(...), allow_local, dns_timeout_seconds. |
validate_egress_url / validate_egress_url_details (+ _async) |
Validate a URL and resolve pinnable addresses. |
build_egress_http_client(url, *, policy) |
Validate + build a DNS-pinned httpx.AsyncClient. |
build_pinned_https_async_client(validated, *, policy) |
Pin an already-validated URL. |
ValidatedEgressURL, EgressNotAllowedError |
Result type and typed failure (a ValueError). |
One source, multi use (OSMU)
egressweave is extracted, behaviour-preserving, from a production control
plane (naruon), where it guards
every LLM-provider call. It is usable both as a standalone dependency and as a
git submodule. The only change on extraction was replacing the app-specific
settings object with an injected EgressPolicy.
Version compatibility
The pinned transport uses a few httpx / httpcore internals, so those
libraries are constrained to httpx>=0.28,<0.29 and httpcore>=1.0,<2.0 and
exercised by the test-suite. Bumping either requires re-verifying the transport.
Research grounding
See docs/research: OWASP SSRF Prevention, CWE-918,
CWE-350 (DNS rebinding / TOCTOU), and RFC 8305 (Happy Eyeballs — the concurrent
connect used across pinned addresses).
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 egressweave-0.1.0.tar.gz.
File metadata
- Download URL: egressweave-0.1.0.tar.gz
- Upload date:
- Size: 15.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e1e3a6dbabd4084fb03f19a95931ab96e4beeef96bc8fc7cf0d8e5b91e266057
|
|
| MD5 |
95822a4df2e7f26442f702c2be4029ab
|
|
| BLAKE2b-256 |
23982302136a7f35d216b4e019d1f3b4b9688bed488a06c1cd9b2022e68aded7
|
File details
Details for the file egressweave-0.1.0-py3-none-any.whl.
File metadata
- Download URL: egressweave-0.1.0-py3-none-any.whl
- Upload date:
- Size: 15.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6bcb07109bdee25a6d49e5516f4c99ecd172ffe8536455d2cac860c44a4492f6
|
|
| MD5 |
4f43c7d4ef471559996e14cec9779483
|
|
| BLAKE2b-256 |
6f5aec82b385d2997796fabaaa3f827b4dc0fe46b466d5a99003e3f69d0a0065
|