safehttp is an HTTP client for Go that checks where requests are allowed to go. It builds on net/http to help protect applications that fetch user-supplied URLs from server-side request forgery (SSRF).
Why?
I regularly need my applications to fetch URLs supplied by a user or customer. A few examples of features that would fetch URLs supplied by a third-party could be an image importer, webhook sender, or link previews.
However, the server may be able to reach internal services (databases, dashboards, etc.) the user cannot access directly. SSRF lets them use the application’s network access to make requests to those services.
Go’s default HTTP client does not restrict access to these destinations. safehttp is a drop-in replacement that has stricter defaults and additional checks to help prevent SSRF.
Where a Request Can Go Wrong
An application intended for one purpose can reach internal services in several ways.
Direct SSRF
A user could supply http://10.0.0.5/admin. If that address belongs to an internal admin dashboard, the application could contact it from inside the server’s private network.
The target could also be a loopback address such as http://127.0.0.1:8080/admin, allowing it to reach an admin dashboard listening only on localhost.
Returning the response can potentially expose data. If an attacker cannot see the response, the request could potentially still trigger an action.
Go’s default client does not reject internal addresses, requiring developers to specifically write code to block such behaviour. safehttp blocks private and loopback destinations by default, even if you configure it to allow HTTP or additional ports.
Special-Purpose Addresses
Other than private networks, safehttp also blocks link-local addresses, multicast, and selected special-purpose ranges based on IANA’s IPv4 and IPv6 registries.
These include shared address space such as 100.64.0.0/10, used for carrier-grade NAT, and ranges reserved for documentation or network benchmarking. Applications that need a blocked range can still explicitly allow it.
Redirect-Based SSRF
An external host can respond with a redirect, asking the client to fetch a different URL. That is useful when a resource, such as an image, has moved, but the replacement could be https://127.0.0.1/admin.
Go’s default redirect policy does limit the maximum number of redirects, but does not reject a target because it is internal.
safehttp instead checks every redirect against the destination policy and checks the IP address used for any new connection. Applications can also configure the maximum number of redirects or choose to disable redirects entirely in ideal cases.
DNS Rebinding
DNS rebinding changes the destination without changing the URL. Before connecting, the client uses DNS to look up the hostname’s IP addresses. A name such as images.example.com can potentially resolve to an internal address.
An application might look up the address first and check that it is public. But the HTTP client may perform another lookup when it connects. An attacker controlling the DNS answers can return a public address for the check, then an internal address for the connection.
Once again, Go’s default transport does not filter out internal addresses returned by DNS.
On the other hand, safehttp does check the address selected for each new connection and refuses blocked destinations, without relying on an earlier lookup.
I recommend Doyensec’s safeurl article, which is a great deep dive resource that explains this approach with Go’s HTTP client.
Proxies and Request Routing
Outbound Proxies
With a proxy, the application connects to an intermediary that contacts the destination on its behalf. Go’s standard client can use proxy settings from the environment, so the application is not always making the final connection itself.
Checking the proxy’s address does not check the destination it will contact. safehttp therefore disables proxies.
Deployments that require one need destination enforcement there and an ideal implementation is Stripe’s Smokescreen, which checks hostnames and IP addresses at the outbound proxy.
Host Header Routing
Routing can also happen at the destination. A reverse proxy forwards requests to a backend, often using the HTTP Host header to choose which one. If it trusts an arbitrary hostname supplied by the caller, a request to a public address could potentially be forwarded to an internal service. This could also be known as routing-based SSRF.
Go does allow applications to override the outgoing Host header, while safehttp rejects an override with a different host or port by default.
public.example.com admin.internal Compared with Go’s Default Client
safehttp.NewClient() returns a standard *http.Client, with Go’s connection pooling and TLS handling. The only difference is the destination checks.
| Concern | Go default client | safehttp default |
|---|---|---|
| Scheme and port | HTTP and HTTPS, no port restrictions | HTTPS on port 443 |
| Internal or special-purpose addresses | No destination filtering | Blocked in URLs and before connecting |
| Redirects | Chain limit, no destination filtering | Chain limit and destination checks |
| Credentials in the URL | Accepted | Rejected |
| Custom Host header | Permitted | Must match the URL’s host and effective port |
| Proxies | Uses environment settings | Disabled |
These defaults are intended for fetching public URLs. Applications can explicitly allow HTTP, other ports, or private address ranges when needed. If a feature only calls one API, it is better to restrict requests to that service rather than allow any public destination. OWASP makes the same distinction between fetching arbitrary URLs and contacting known services.
Acknowledgements
safehttp was inspired by and borrows from Doyensec’s safeurl and daenney/ssrf. Both are worth reading if you are interested in SSRF protection in Go.