Skip to content

safehttp: SSRF-safe transport for client and fetch#24

Merged
andrew merged 1 commit into
mainfrom
safehttp-transport
May 12, 2026
Merged

safehttp: SSRF-safe transport for client and fetch#24
andrew merged 1 commit into
mainfrom
safehttp-transport

Conversation

@andrew
Copy link
Copy Markdown
Contributor

@andrew andrew commented May 12, 2026

Summary

Adds an HTTP transport that hardens registry fetches against SSRF, DNS rebinding, and redirect-target exfiltration. Surfaced as a new `registries/safehttp` package plus a `client.WithSafeHTTP()` option; the `fetch` package's existing dnscache-backed dialer now gates each resolved IP against the block list.

Threats

  • SSRF via redirect — a registry or CDN that returns a 30x to `http://localhost\` / RFC1918 / link-local could probe internal services.
  • DNS rebinding — the IP returned by DNS at resolve time isn't necessarily what gets dialed; an attacker who controls DNS can swap to a private address between resolve and connect.
  • Redirect-target exfiltration — `Location: file:///etc/passwd` is a class of attack worth refusing categorically.

Defences

  1. Dial-time IP gate. DNS is resolved once per dial; each resolved address is checked against the block list (loopback, RFC1918, CGNAT 100.64.0.0/10, link-local, multicast, unspecified) before any TCP connect. The connection then dials the resolved IP directly, so a rebind between check and connect cannot escape.
  2. Redirect cap at 10 hops, re-validating every target through the same dial gate.
  3. Non-http(s) schemes (`file://`, `gopher://`, `data://`) rejected on redirect.

API

```go
c := client.NewClient(client.WithSafeHTTP())
```

`fetch.NewFetcher` picks up the IP gate automatically — no API change for fetch consumers.

Test opt-out

`safehttp.EnableLoopbackForTesting()` (called from TestMain) flips loopback + private-IP gates off for the test binary so existing `httptest.Server`-backed test suites continue to work. Production paths never see this.

Adds an HTTP transport that hardens registry fetches against three
related threats:

  * Server-Side Request Forgery via a registry / CDN that 30x's to an
    internal address (http://localhost, RFC1918, link-local, ...) so
    the client probes services it shouldn't reach.
  * DNS rebinding between resolve and connect — the IP that comes back
    from DNS is not necessarily what gets connected to if the resolver
    cache or upstream is poisoned.
  * Redirect-target exfiltration via non-http(s) schemes (file://,
    gopher://, data://) returned in a Location header.

Three defences in concert:

  1. Dial-time IP gate. DNS is resolved once per dial; each resolved
     address is checked against the block list (loopback, RFC1918,
     CGNAT 100.64.0.0/10, link-local, multicast, unspecified) before
     any TCP connect. The connection then dials the resolved IP
     directly, so a rebind between check and connect cannot escape.
  2. Redirect cap at 10 hops, re-validating every target through the
     same dial gate.
  3. Non-http(s) scheme rejected on redirect.

New package github.com/git-pkgs/registries/safehttp holds the
transport. client.WithSafeHTTP() opts a Client into it. The fetch
package's existing dnscache-backed dialer now gates each resolved IP
against safehttp.CheckIP before connecting.

Loopback opt-out via safehttp.EnableLoopbackForTesting (called from
TestMain) keeps existing httptest.Server-backed test suites working;
production paths never see the opt-out.
@andrew andrew force-pushed the safehttp-transport branch from ac4054f to ba424c1 Compare May 12, 2026 17:16
@andrew andrew merged commit 397d12c into main May 12, 2026
5 checks passed
@andrew andrew deleted the safehttp-transport branch May 12, 2026 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant