{"record":{"id":"572892170ea8f795","repo":"JuliusBrussee/caveman","slug":"ssrf-credentials-embedded-in-url-are-forbidden","errorCode":null,"errorMessage":"ssrf: credentials embedded in URL are forbidden","messagePattern":"ssrf: credentials embedded in URL are forbidden","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/ssrf/ssrf.go","lineNumber":170,"sourceCode":"\n// ValidateURL resolves raw to a URL, validates the scheme/port constraints,\n// and checks every IP the hostname resolves to against the SSRF block lists.\n// It is a pre-flight check only — see NewDialContext for dial-time enforcement.\n//\n// Errors are safe to return to callers; they contain the blocked IP but never\n// the original credential material.\nfunc ValidateURL(ctx context.Context, raw string, cfg Config) error {\n\tu, err := url.Parse(raw)\n\tif err != nil {\n\t\t// net/url.Error includes the raw URL (and may therefore include\n\t\t// credentials or query secrets). Keep this error field-only and stable.\n\t\treturn errors.New(\"ssrf: invalid URL\")\n\t}\n\tif u.Scheme != \"https\" && !(u.Scheme == \"http\" && !cfg.ManagedMode) {\n\t\treturn fmt.Errorf(\"ssrf: scheme %q not permitted (managed mode requires https)\", u.Scheme)\n\t}\n\tif u.User != nil {\n\t\treturn fmt.Errorf(\"ssrf: credentials embedded in URL are forbidden\")\n\t}\n\thost := u.Hostname()\n\tif host == \"\" {\n\t\treturn fmt.Errorf(\"ssrf: URL must contain a host\")\n\t}\n\tport := u.Port()\n\tif cfg.ManagedMode && port != \"\" && port != \"443\" {\n\t\treturn errors.New(\"ssrf: managed mode requires port 443\")\n\t}\n\tif port == \"\" {\n\t\tif u.Scheme == \"https\" {\n\t\t\tport = \"443\"\n\t\t} else {\n\t\t\tport = \"80\"\n\t\t}\n\t}\n\treturn validateHostPort(ctx, host, port, cfg)\n}","sourceCodeStart":152,"sourceCodeEnd":188,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/shared/platform/ssrf/ssrf.go#L152-L188","documentation":"ssrf.ValidateURL rejects URLs containing userinfo (u.User != nil), i.e. https://user:pass@host/... style URLs. Embedded credentials leak into logs, referrers, and error messages, so the library forbids them outright regardless of mode.","triggerScenarios":"Calling ssrf.ValidateURL with any URL whose authority section contains 'user:pass@' or 'user@' before the host.","commonSituations":"Users pasting an API-key-in-URL from a provider dashboard (e.g. https://KEY@api.example.com/v1); basic-auth URLs copied from curl examples; internal endpoints documented with embedded basic auth.","solutions":["Move the credential to a header (Authorization: Bearer/Basic) or the API's documented auth parameter.","Strip userinfo before persisting URLs: parse with net/url, set u.User = nil, re-serialize.","Add ingestion-time validation so credential-bearing URLs never reach storage."],"exampleFix":"// before\nraw := \"https://\" + apiKey + \"@api.example.com/v1\"\nerr := ssrf.ValidateURL(ctx, raw, cfg) // rejected\n\n// after\nreq, _ := http.NewRequest(\"GET\", \"https://api.example.com/v1\", nil)\nreq.Header.Set(\"Authorization\", \"Bearer \"+apiKey)\nerr := ssrf.ValidateURL(ctx, \"https://api.example.com/v1\", cfg)","handlingStrategy":"validation","validationCode":"u, err := url.Parse(raw)\nif err == nil && u.User != nil {\n    return fmt.Errorf(\"credentials in URL are not allowed; pass them as a header\")\n}","typeGuard":"func hasNoUserinfo(raw string) bool {\n    u, err := url.Parse(raw)\n    return err == nil && u.User == nil\n}","tryCatchPattern":"if err := ssrf.ValidateURL(ctx, raw, cfg); err != nil {\n    if strings.Contains(err.Error(), \"credentials embedded\") {\n        // strip userinfo and move to Authorization header, then retry\n    }\n}","preventionTips":["Normalize stored URLs: parse, set u.User = nil, re-serialize before persisting.","Teach users header-based auth (Bearer/Basic) in integration docs."],"tags":["ssrf","security","credentials","validation","go"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}