JuliusBrussee/caveman · error

ssrf: direct request to the configured proxy address is not…

Error message

ssrf: direct request to the configured proxy address is not permitted

What it means

In the proxied transport hook, when cfg.Proxy returns nil for a request (meaning "dial directly"), the library checks whether the request URL targets one of the proxy's own addresses. If it does, the request is rejected: dialing the proxy directly would loop or bypass the guard, so direct requests to the configured proxy address are forbidden.

Solutions

  1. Remove the request that targets the proxy address itself — clients of this library must not fetch the proxy endpoint.
  2. Point health checks/metrics scraping for the proxy at a non-HTTP path (or a dedicated client not built via the ssrf package).
  3. Move the internal service to a different address/port so it no longer collides with proxyDialAddr(req.URL).

Example fix

// before
resp, err := proxiedClient.Get("http://egress-proxy.internal:3128/health") // direct hit on proxy
// after
if isProxyAddr(targetURL) {
    return errors.New("target is the egress proxy; use a plain client for proxy health checks")
}
resp, err := plainClient.Get("http://egress-proxy.internal:3128/health")
Defensive patterns

Strategy: validation

Validate before calling

if hostPort(targetURL) == proxyAddr {
    return errors.New("target is the configured proxy; use a non-guarded client for proxy endpoints")
}

Try / catch

resp, err := client.Do(req)
if err != nil && strings.Contains(err.Error(), "direct request to the configured proxy address") {
    return fmt.Errorf("refusing self-referential fetch of the proxy endpoint: %w", err)
}

Prevention

When it happens

Trigger: Using a client with cfg.Proxy set; cfg.Proxy(req) returns nil (no proxy for this request) AND req.URL's host:port equals an address in proxyAddrs (the proxy dial address computed via proxyDialAddr) — e.g. fetching the proxy's own health endpoint or a webhook that happens to point at the proxy host.

Common situations: Proxy self-tests or health checks pointed at the proxy URL; a redirect (3xx) leading the client to the proxy host; NO_PROXY covering the proxy host itself so requests to it go direct; misconfigured internal service sharing the proxy's address.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/2e3c4d825deb2ba2. Report an issue: GitHub.

Appendix: source

Thrown at shared/platform/ssrf/ssrf.go:571

// proxiedHooks returns the Transport.Proxy and DialContext pair for a client
// with cfg.Proxy set. The proxy hook validates what it can about the request
// destination without DNS (the proxy resolves hostnames, often on a network the
// client cannot see), then remembers the proxy address it chose so the dial hook
// can pass that one address through unguarded. Every other address — including
// a direct dial when cfg.Proxy returns nil, e.g. a NO_PROXY match — still goes
// through the full guard. The dial hook cannot tell a proxied dial from a direct
// one to the same host:port, so a direct request whose destination collides with
// a remembered proxy address is refused here instead of reaching that pass-through.
func proxiedHooks(cfg Config, guarded func(context.Context, string, string) (net.Conn, error)) (func(*http.Request) (*url.URL, error), func(context.Context, string, string) (net.Conn, error)) {
	var proxyAddrs sync.Map // host:port as Transport dials it → struct{}
	proxy := func(req *http.Request) (*url.URL, error) {
		u, err := cfg.Proxy(req)
		if err != nil {
			return nil, err
		}
		if u == nil {
			if _, collides := proxyAddrs.Load(proxyDialAddr(req.URL)); collides {
				return nil, errors.New("ssrf: direct request to the configured proxy address is not permitted")
			}
			return nil, nil
		}
		if err := ValidateURLNoResolve(req.URL.String(), cfg); err != nil {
			return nil, err
		}
		proxyAddrs.Store(proxyDialAddr(u), struct{}{})
		return u, nil
	}
	raw := newDialer(cfg).DialContext
	dial := func(ctx context.Context, network, addr string) (net.Conn, error) {
		if _, ok := proxyAddrs.Load(addr); ok {
			return raw(ctx, network, addr)
		}
		return guarded(ctx, network, addr)
	}
	return proxy, dial
}

View on GitHub (pinned to 3ee70a1026)