{"record":{"id":"f71c8528402c901d","repo":"MHSanaei/3x-ui","slug":"url-host-is-required","errorCode":null,"errorMessage":"URL host is required","messagePattern":"URL host is required","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/web/service/url_safety.go","lineNumber":29,"sourceCode":"\t\"github.com/mhsanaei/3x-ui/v3/internal/util/netsafe\"\n)\n\n// SanitizeHTTPURL validates and normalizes an http(s) URL without resolving\n// DNS. Use SanitizePublicHTTPURL at the point of an outbound request.\nfunc SanitizeHTTPURL(raw string) (string, error) {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn \"\", nil\n\t}\n\tu, err := url.Parse(raw)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif u.Scheme != \"http\" && u.Scheme != \"https\" {\n\t\treturn \"\", fmt.Errorf(\"unsupported URL scheme %q\", u.Scheme)\n\t}\n\tif u.Host == \"\" || u.Hostname() == \"\" {\n\t\treturn \"\", fmt.Errorf(\"URL host is required\")\n\t}\n\tclean := &url.URL{\n\t\tScheme:   u.Scheme,\n\t\tHost:     u.Host,\n\t\tPath:     u.Path,\n\t\tRawPath:  u.RawPath,\n\t\tRawQuery: u.RawQuery,\n\t\tFragment: u.Fragment,\n\t}\n\treturn clean.String(), nil\n}\n\n// SanitizePublicHTTPURL validates and normalizes an http(s) URL, then blocks\n// private/internal targets unless the caller explicitly allows them.\nfunc SanitizePublicHTTPURL(raw string, allowPrivate bool) (string, error) {\n\tclean, err := SanitizeHTTPURL(raw)\n\tif err != nil || clean == \"\" {\n\t\treturn clean, err","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/MHSanaei/3x-ui/blob/ad32144c42455696ea9f14e12168beac3e25f5d2/internal/web/service/url_safety.go#L11-L47","documentation":"SanitizeHTTPURL requires a non-empty Host with a parseable hostname. It fires when the scheme is http(s) but url.Parse found no authority component — inputs like 'https:///path' (empty host), 'http://' (nothing at all), or 'https:/path' (single slash: url.Parse treats it as path-only on older Go stdlib shapes).","triggerScenarios":"Storing 'http://' + an empty address field; concatenation bugs like \"http://\" + host where host is empty; user typing 'https:/example.com' (missing slash).","commonSituations":"Frontend forms submitting before the user filled the address; templating/config code joining scheme and host where the host variable is empty; trailing-colon typos.","solutions":["Require and validate the host field at input time (non-empty, matches a hostname regex) before building the URL.","Fix the concatenation site: check the host variable is non-empty before fmt.Sprintf(\"https://%s/...\", host).","Trim input — leading whitespace can make url.Parse put the whole string into Path."],"exampleFix":"// before\nurl := fmt.Sprintf(\"https://%s/ping\", host) // host == \"\" -> \"https:///ping\"\n\n// after\nif strings.TrimSpace(host) == \"\" {\n    return errors.New(\"host is required\")\n}\nurl := fmt.Sprintf(\"https://%s/ping\", host)","handlingStrategy":"validation","validationCode":"// Build URLs only from a validated non-empty host\nhost := strings.TrimSpace(host)\nif host == \"\" {\n    return errors.New(\"host is required\")\n}\ntarget := \"https://\" + host + path","typeGuard":"func hasHost(raw string) bool {\n    u, err := url.Parse(strings.TrimSpace(raw))\n    return err == nil && u.Hostname() != \"\"\n}","tryCatchPattern":"clean, err := service.SanitizeHTTPURL(raw)\nif err != nil && strings.Contains(err.Error(), \"host is required\") {\n    // prompt user for the address; do not default to localhost\n}","preventionTips":["Never fmt.Sprintf a scheme onto an unvalidated host variable.","Trim and require non-empty host in form validation before submit."],"tags":["url","validation","ssrf"],"backgroundTag":null,"analyzedSha":"ad32144c42455696ea9f14e12168beac3e25f5d2","analyzedAt":"2026-08-15T11:13:23.905Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}