•
6 min read

safehttp

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.

An internal request through the application server Without destination restrictions, a user supplies an internal URL to the application server. The server fetches it using its own network access, potentially reaching an internal service at 10.0.0.5 that the user cannot reach directly. This shows the risk, not a request permitted by safehttp. Private network User Supplies URL Application server Fetches URL Internal service 10.0.0.5
The server can reach destinations the user cannot.

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.

A redirect is a new destination A public host returns a redirect to https://127.0.0.1/admin. The redirect target is checked and rejected because it is loopback. No connection is made to that target. Public host302 Found Location: https://127.0.0.1/admin Check targetLoopback Blocked No request to the redirect target
Each redirect needs a new destination check.

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.

One hostname, two DNS answers The hostname images.example.com stays the same. An earlier application lookup returns a public IP and passes validation. A later client lookup returns the private IP 10.0.0.5. Relying on the earlier check would allow an internal connection. safehttp instead checks the address selected for the connection and blocks it, without needing the earlier lookup. images.example.com Earlier validation Connection attempt Public IP First DNS answer 10.0.0.5 New DNS answer Check passes safehttp blocks Private IP, no connection
The hostname stays the same. The destination changes.

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.

The proxy makes the destination connection The application connects to a proxy. The proxy makes a separate connection to the destination. Validating the proxy address does not validate that destination. safehttp disables proxies. Application HTTP client Proxy Forwards request Destination Client connection Proxy connection
Checking the proxy does not check the destination.

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.

URL host public.example.com
Public reverse proxy
Overridden Host admin.internal
Internal service
A proxy that trusts the overridden Host could forward internally. safehttp rejects this mismatch before sending the request.

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.

ConcernGo default clientsafehttp default
Scheme and portHTTP and HTTPS, no port restrictionsHTTPS on port 443
Internal or special-purpose addressesNo destination filteringBlocked in URLs and before connecting
RedirectsChain limit, no destination filteringChain limit and destination checks
Credentials in the URLAcceptedRejected
Custom Host headerPermittedMust match the URL’s host and effective port
ProxiesUses environment settingsDisabled

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.