{"record":{"id":"2e3c4d825deb2ba2","repo":"JuliusBrussee/caveman","slug":"ssrf-direct-request-to-the-configured-proxy-address-is-not","errorCode":null,"errorMessage":"ssrf: direct request to the configured proxy address is not permitted","messagePattern":"ssrf: direct request to the configured proxy address is not permitted","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/ssrf/ssrf.go","lineNumber":571,"sourceCode":"// proxiedHooks returns the Transport.Proxy and DialContext pair for a client\n// with cfg.Proxy set. The proxy hook validates what it can about the request\n// destination without DNS (the proxy resolves hostnames, often on a network the\n// client cannot see), then remembers the proxy address it chose so the dial hook\n// can pass that one address through unguarded. Every other address — including\n// a direct dial when cfg.Proxy returns nil, e.g. a NO_PROXY match — still goes\n// through the full guard. The dial hook cannot tell a proxied dial from a direct\n// one to the same host:port, so a direct request whose destination collides with\n// a remembered proxy address is refused here instead of reaching that pass-through.\nfunc 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)) {\n\tvar proxyAddrs sync.Map // host:port as Transport dials it → struct{}\n\tproxy := func(req *http.Request) (*url.URL, error) {\n\t\tu, err := cfg.Proxy(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif u == nil {\n\t\t\tif _, collides := proxyAddrs.Load(proxyDialAddr(req.URL)); collides {\n\t\t\t\treturn nil, errors.New(\"ssrf: direct request to the configured proxy address is not permitted\")\n\t\t\t}\n\t\t\treturn nil, nil\n\t\t}\n\t\tif err := ValidateURLNoResolve(req.URL.String(), cfg); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tproxyAddrs.Store(proxyDialAddr(u), struct{}{})\n\t\treturn u, nil\n\t}\n\traw := newDialer(cfg).DialContext\n\tdial := func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\tif _, ok := proxyAddrs.Load(addr); ok {\n\t\t\treturn raw(ctx, network, addr)\n\t\t}\n\t\treturn guarded(ctx, network, addr)\n\t}\n\treturn proxy, dial\n}","sourceCodeStart":553,"sourceCodeEnd":589,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/shared/platform/ssrf/ssrf.go#L553-L589","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before\nresp, err := proxiedClient.Get(\"http://egress-proxy.internal:3128/health\") // direct hit on proxy\n// after\nif isProxyAddr(targetURL) {\n    return errors.New(\"target is the egress proxy; use a plain client for proxy health checks\")\n}\nresp, err := plainClient.Get(\"http://egress-proxy.internal:3128/health\")","handlingStrategy":"validation","validationCode":"if hostPort(targetURL) == proxyAddr {\n    return errors.New(\"target is the configured proxy; use a non-guarded client for proxy endpoints\")\n}","typeGuard":null,"tryCatchPattern":"resp, err := client.Do(req)\nif err != nil && strings.Contains(err.Error(), \"direct request to the configured proxy address\") {\n    return fmt.Errorf(\"refusing self-referential fetch of the proxy endpoint: %w\", err)\n}","preventionTips":["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"],"tags":["ssrf","proxy","loop-prevention","go"],"backgroundTag":"unsupported-operation","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}