{"record":{"id":"80c72742116e17a4","repo":"JuliusBrussee/caveman","slug":"ssrf-config-proxy-is-not-supported-in-managed-mode","errorCode":null,"errorMessage":"ssrf: Config.Proxy is not supported in managed mode","messagePattern":"ssrf: Config\\.Proxy is not supported in managed mode","errorType":"exception","errorClass":"ErrProxyInManagedMode","httpStatus":null,"severity":"error","filePath":"shared/platform/ssrf/ssrf.go","lineNumber":545,"sourceCode":"\t\t\t// exactly what it was; only the diagnosis changes.\n\t\t\treturn &http.Client{Transport: refusingTransport{err: ErrProxyInManagedMode}}\n\t\t}\n\t\tt.Proxy, t.DialContext = proxiedHooks(cfg, t.DialContext)\n\t}\n\treturn &http.Client{\n\t\tTransport: t,\n\t\t// Provider and webhook clients must not carry credentials across redirects.\n\t\t// Callers that intentionally implement redirects (for example OIDC) must\n\t\t// validate every hop explicitly and use their own bounded policy.\n\t\tCheckRedirect: func(_ *http.Request, _ []*http.Request) error {\n\t\t\treturn http.ErrUseLastResponse\n\t\t},\n\t}\n}\n\n// ErrProxyInManagedMode reports a wiring bug: a managed-mode Config carrying a\n// Proxy selector. Managed mode never proxies (see Config.Proxy).\nvar ErrProxyInManagedMode = errors.New(\"ssrf: Config.Proxy is not supported in managed mode\")\n\n// refusingTransport fails every request with one explanatory error instead of\n// letting a client built from a contradictory Config look like it works.\ntype refusingTransport struct{ err error }\n\nfunc (t refusingTransport) RoundTrip(*http.Request) (*http.Response, error) { return nil, t.err }\n\n// 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{}","sourceCodeStart":527,"sourceCodeEnd":563,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/shared/platform/ssrf/ssrf.go#L527-L563","documentation":"ErrProxyInManagedMode signals a wiring bug: a Config with ManagedMode=true also set Config.Proxy. Managed mode never routes through a proxy, so instead of silently ignoring the proxy or opening a hole, NewHTTPClient returns an http.Client whose refusingTransport fails every request with this exact error. No network I/O occurs; the security posture is unchanged and only the diagnosis is explicit.","triggerScenarios":"Calling ssrf.NewHTTPClient with a Config where both ManagedMode=true and Proxy != nil, then performing any request — every RoundTrip returns this error wrapped as the request error (checkable with errors.Is).","commonSituations":"Merging config structs so a proxy set for a non-managed environment bleeds into the managed production client; copying a client-construction snippet that always sets Proxy from env (HTTP_PROXY-derived selector) while forcing managed mode; feature-flag combination (managed + egress proxy) the library disallows.","solutions":["Remove cfg.Proxy when ManagedMode is true — managed mode forbids proxying by design.","Check with errors.Is(err, ssrf.ErrProxyInManagedMode) and fail fast at startup: detect the contradictory config when building the client rather than at first request.","If a proxy is genuinely required (e.g. operator egress proxy), run the client in non-managed mode with the proxy hooks, keeping the literal-proxy-address guard."],"exampleFix":"// before\ncfg := ssrf.Config{ManagedMode: true, Proxy: http.ProxyFromEnvironment}\nclient, err := ssrf.NewHTTPClient(cfg) // every request fails with ErrProxyInManagedMode\n// after\ncfg := ssrf.Config{ManagedMode: true}\nif cfg.ManagedMode && cfg.Proxy != nil {\n    return errors.New(\"proxy and managed mode are mutually exclusive\")\n}\nclient, err := ssrf.NewHTTPClient(cfg)","handlingStrategy":"try-catch","validationCode":"if cfg.ManagedMode && cfg.Proxy != nil {\n    return errors.New(\"ssrf.Config: Proxy and ManagedMode are mutually exclusive\")\n}","typeGuard":null,"tryCatchPattern":"client, err := ssrf.NewHTTPClient(cfg)\nif err != nil { return err }\nresp, err := client.Do(req)\nif errors.Is(err, ssrf.ErrProxyInManagedMode) {\n    return fmt.Errorf(\"config bug: proxy set while managed mode is on: %w\", err)\n}","preventionTips":["Detect the contradictory config at client-construction/startup, not first request","Gate Proxy selection on the same flag that disables ManagedMode","Avoid blanket-copying env-derived proxy settings into every client config"],"tags":["ssrf","proxy","config-conflict","go"],"backgroundTag":"conflicting-config-options","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"}