{"record":{"id":"ebb56e376edace00","repo":"JuliusBrussee/caveman","slug":"ssrf-url-must-contain-a-host","errorCode":null,"errorMessage":"ssrf: URL must contain a host","messagePattern":"ssrf: URL must contain a host","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/ssrf/ssrf.go","lineNumber":174,"sourceCode":"//\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}\n\n// ValidateHost resolves host (bare hostname or IP literal) and checks all\n// resolved addresses.  Use when you have a host/port pair rather than a full\n// URL.","sourceCodeStart":156,"sourceCodeEnd":192,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/shared/platform/ssrf/ssrf.go#L156-L192","documentation":"ssrf.ValidateURL requires a non-empty host component (u.Hostname()). URLs like 'https:///path', 'https://?q=1', or scheme-only strings parse successfully but have no host, so the guard rejects them before DNS resolution.","triggerScenarios":"Calling ssrf.ValidateURL with a URL whose authority is empty: 'https:///health', 'https:/health', relative paths like '/api/hook', or empty strings that still parse.","commonSituations":"Building URLs by string concatenation where the host variable is empty (unset env var); users entering only a path in a webhook field; a config default of '' slipping through.","solutions":["Check the host/env var is set before constructing the URL.","Require a full absolute URL at the input layer (form field placeholder/validation) and reject relative paths.","Default the host explicitly (e.g. api.example.com) instead of concatenating possibly-empty values."],"exampleFix":"// before\nu := \"https://\" + os.Getenv(\"PROVIDER_HOST\") + \"/v1\" // host unset -> https:///v1\n\n// after\nhost := os.Getenv(\"PROVIDER_HOST\")\nif host == \"\" { return errors.New(\"PROVIDER_HOST not set\") }\nu := \"https://\" + host + \"/v1\"","handlingStrategy":"validation","validationCode":"u, err := url.Parse(raw)\nif err != nil || u.Hostname() == \"\" {\n    return fmt.Errorf(\"a complete absolute URL with host is required\")\n}","typeGuard":"func hasHost(raw string) bool {\n    u, err := url.Parse(raw)\n    return err == nil && u.Hostname() != \"\"\n}","tryCatchPattern":null,"preventionTips":["Require absolute URLs in webhook/endpoint fields and validate on save.","Fail fast on empty host env vars before string-concatenating URLs."],"tags":["ssrf","validation","url","go"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}