{"record":{"id":"b90a1c55041d511d","repo":"JuliusBrussee/caveman","slug":"provider-q-upstream-url-must-not-include-userinfo","errorCode":null,"errorMessage":"provider %q upstream URL must not include userinfo","messagePattern":"provider %q upstream URL must not include userinfo","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"proxy/providers/openaicompat/openaicompat.go","lineNumber":526,"sourceCode":"\treturn nil\n}\n\nfunc parseBaseURL(raw, provider string) (*url.URL, error) {\n\tif strings.TrimSpace(raw) == \"\" {\n\t\treturn nil, fmt.Errorf(\"provider %q has no configured upstream URL\", provider)\n\t}\n\tu, err := url.Parse(strings.TrimSpace(raw))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif u.Scheme != \"http\" && u.Scheme != \"https\" {\n\t\treturn nil, fmt.Errorf(\"provider %q upstream URL scheme %q is not allowed\", provider, u.Scheme)\n\t}\n\tif !u.IsAbs() || u.Host == \"\" || u.Hostname() == \"\" {\n\t\treturn nil, fmt.Errorf(\"provider %q upstream URL must be an absolute URL with a host\", provider)\n\t}\n\tif u.User != nil {\n\t\treturn nil, fmt.Errorf(\"provider %q upstream URL must not include userinfo\", provider)\n\t}\n\tif u.Fragment != \"\" {\n\t\treturn nil, fmt.Errorf(\"provider %q upstream URL must not include a fragment\", provider)\n\t}\n\tif err := validatePathComponents(u.Path, u.RawPath); err != nil {\n\t\treturn nil, fmt.Errorf(\"provider %q upstream URL path rejected: %w\", provider, err)\n\t}\n\treturn u, nil\n}\n","sourceCodeStart":508,"sourceCodeEnd":536,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/proxy/providers/openaicompat/openaicompat.go#L508-L536","documentation":"parseBaseURL rejects an upstream base URL that embeds userinfo (an 'user:password@' component before the host), openaicompat.go:525. Credentials in the base URL would be silently forwarded and joined into every upstream request, leaking them into logs and metering paths. The adapter strips nothing, so it refuses the value outright.","triggerScenarios":"Configure a compat provider with a base URL such as 'https://user:token@api.example.com/v1' or 'https://apikey@api.example.com' and then call ResolveUpstreamURL or ValidateBaseURL.","commonSituations":"Copying a curl-style credentialized URL from vendor docs or an old client config; some self-hosted gateways (LiteLLM, one-api) document 'key@host' style URLs; users also embed tokens this way to 'remember' them.","solutions":["Remove the 'user:password@' portion from the base URL and keep only 'scheme://host[:port]/path'.","Pass the credential through the proxy's dedicated credential mechanism (BYOK env key or inbound Authorization/x-api-key) instead of the URL.","Re-run ValidateBaseURL on the cleaned value to confirm it passes."],"exampleFix":"// before\nbase_url: https://sk-abc123@api.example.com/v1\n// after\nbase_url: https://api.example.com/v1\n# supply the key via the provider credential/env-key resolution instead","handlingStrategy":"validation","validationCode":"if u, err := url.Parse(raw); err == nil && u.User != nil {\n    return fmt.Errorf(\"base URL must not contain user:pass@ — move the credential to the auth mechanism\")\n}","typeGuard":"func hasUserInfo(raw string) bool {\n    u, err := url.Parse(raw)\n    return err == nil && u.User != nil\n}","tryCatchPattern":"if err := ValidateBaseURL(raw); err != nil {\n    if strings.Contains(err.Error(), \"userinfo\") {\n        log.Fatal(\"strip the user:token@ prefix from base_url and configure the key via credentials instead\")\n    }\n    log.Fatal(err)\n}","preventionTips":["Never embed API keys in URLs, even for self-hosted gateways that accept key@host.","Store provider keys in env-based BYOK resolution or the request credential header, not the base URL.","Grep config files for the '@' character inside base_url values during review."],"tags":["go","url-validation","security","credentials"],"backgroundTag":"invalid-url-format","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"}