Fetch user-supplied URLs in Python without opening an SSRF hole. Standard library only, no dependencies, one file.
from safe_fetch import fetch, SafeFetchError
try:
response = fetch("https://example.com")
print(response["status"], response["headers"]["content-type"])
except SafeFetchError as e:
print("refused:", e)Does your application fetch a URL that a user gave it? A webhook tester, a link preview, an avatar importer, an RSS reader, a "check my website" tool, an image proxy?
Then this works, and it shouldn't:
requests.get(user_supplied_url) # user types http://169.254.169.254/latest/meta-data/That address is the cloud instance metadata service on AWS, GCP and Azure. Depending on configuration it hands back IAM credentials. Your server has access to it; the attacker doesn't — until your code fetches it for them and returns the response.
This is Server-Side Request Forgery, and it's in the OWASP Top 10. The same trick reaches 127.0.0.1:6379 (your Redis), 192.168.x.x (everything else on your network), and file:///etc/passwd if the library follows that scheme.
The standard library will connect anywhere you point it. Nothing stops it by default.
fetch("http://169.254.169.254/latest/meta-data/")
# SafeFetchError: '169.254.169.254' resolves to 169.254.169.254,
# which is a private or internal address.Specifically:
- Resolves the hostname and checks every returned address before connecting. Not just the first one — a hostname can resolve to several, and an attacker controlling DNS can return one public and one private address hoping you check the wrong one.
- Blocks private, loopback, link-local, multicast, reserved and unspecified ranges, for both IPv4 and IPv6, including IPv4-mapped IPv6 addresses.
- Re-validates every redirect hop. This is the step most implementations miss. A perfectly innocent public URL that responds 302 Location: http://169.254.169.254/defeats any check performed only on the URL the user typed.
- Allows only http and https. file://,gopher://anddict://are standard SSRF escalation paths.
- Strips embedded credentials, query strings and fragments before the request goes out.
- Caps the response body so a malicious server can't stream you 40 GB.
There's no package. It's one file with no dependencies — copy safe_fetch.py into your project.
curl -O https://raw.githubusercontent.com/Rehanfaisal/ssrf-safe-fetch/main/safe_fetch.pyRequires Python 3.7+.
fetch(url,
timeout=8,
max_redirects=5,
max_body=65536,
user_agent="safe-fetch/1.0.0",
headers=None)Returns a dict:
Raises SafeFetchError for anything refused or failed. It never raises anything else for network conditions, so one except covers it.
Returns True if an address should never be connected to on a user's behalf. Useful on its own if you're doing your own connecting.
Resolves a hostname, raises SafeFetchError if any resolved address is internal, otherwise returns the address list.
Validates the scheme, strips credentials/query/fragment, adds https:// to a bare hostname. Raises SafeFetchError if the URL can't be made safe.
Being honest about the boundaries matters more than a longer feature list.
DNS rebinding. There's a window between the check and the connection. An attacker controlling a DNS server with a one-second TTL can return a public address for your check and a private one for your connection. Closing this completely means resolving once, connecting to the pinned IP, and passing the hostname separately for TLS SNI and the Host header. That's intrusive to do with urllib, so this library doesn't. For most applications the check here is proportionate; if you're handling genuinely hostile input, pin the address.
Your own public IP. If your server has a public address and also runs something you don't want reached, this won't stop it — the address isn't private. Use a firewall as well.
Application-level abuse. This stops your server reaching internal addresses. It doesn't stop someone using it to hammer a third party. Add rate limiting.
Being a proxy. If you return the fetched body verbatim to the user, you've built an open proxy even with these checks. Return only what you need.
python tests/test_safe_fetch.pyNo pytest needed, though it works under pytest too. The suite covers every blocked range, IPv4-mapped IPv6, scheme rejection, credential stripping, and refusal of internal targets by name and by address.
Extracted from the scanner behind IsSiteSafe, a set of free security tools. That site lets anyone type in a URL and have the server fetch it, which is exactly the situation this module exists to make safe.
MIT. Do what you like with it.