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
- Remove the request that targets the proxy address itself — clients of this library must not fetch the proxy endpoint.
- Point health checks/metrics scraping for the proxy at a non-HTTP path (or a dedicated client not built via the ssrf package).
- 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
- Exclude proxy addresses from any allowlist of fetchable targets
- Point proxy health checks at a dedicated unguarded client
- Cover the proxy host in NO_PROXY expectations and test redirect chains don't land on it
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
- ssrf: Config.Proxy is not supported in managed mode
- compat request URL is missing
- compat route path rejected
- compat route does not match default /compat/ mount
- compat route does not match prefix
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)