{"record":{"id":"95c28798f353475d","repo":"projectdiscovery/katana","slug":"publicsuffix-empty-label-in-domain-q","errorCode":null,"errorMessage":"publicsuffix: empty label in domain %q","messagePattern":"publicsuffix: empty label in domain %q","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"pkg/utils/scope/scope.go","lineNumber":168,"sourceCode":"}\n\n// matchesDomainOrSubdomain reports whether host equals domain or is one of its\n// subdomains (i.e. host ends with \".\"+domain). Matching is case-insensitive\n// because DNS labels are case-insensitive. Requiring the leading dot enforces a\n// label boundary, so look-alike hosts that only share domain as a raw string\n// suffix (e.g. evilexample.com vs example.com) are not considered in scope.\nfunc matchesDomainOrSubdomain(host, domain string) bool {\n\thost = strings.ToLower(host)\n\tdomain = strings.ToLower(domain)\n\treturn host == domain || strings.HasSuffix(host, \".\"+domain)\n}\n\n// getDomainRDNandRDN extracts and returns the root domain name (RDN) and the\n// effective top-level domain plus one label (eTLD+1) from the given hostname.\n// It returns empty strings and an error if the hostname cannot be parsed.\nfunc getDomainRDNandRDN(domain string) (string, string, error) {\n\tif strings.HasPrefix(domain, \".\") || strings.HasSuffix(domain, \".\") || strings.Contains(domain, \"..\") {\n\t\treturn \"\", \"\", fmt.Errorf(\"publicsuffix: empty label in domain %q\", domain)\n\t}\n\tsuffix, _ := publicsuffix.PublicSuffix(domain)\n\tif len(domain) <= len(suffix) {\n\t\treturn domain, \"\", nil\n\t}\n\ti := len(domain) - len(suffix) - 1\n\tif domain[i] != '.' {\n\t\treturn domain, \"\", nil\n\t}\n\treturn domain[1+strings.LastIndex(domain[:i], \".\"):], domain[1+strings.LastIndex(domain[:i], \".\") : len(domain)-len(suffix)-1], nil\n}\n","sourceCodeStart":150,"sourceCodeEnd":180,"githubUrl":"https://github.com/projectdiscovery/katana/blob/e3e742739c3746f085943ce918fb4e2b8daf6fe6/pkg/utils/scope/scope.go#L150-L180","documentation":"getDomainRDNandRDN extracts the root domain and eTLD+1 from a hostname, but first rejects hostnames containing an empty label — a leading dot, trailing dot, or consecutive dots. Such input would produce nonsense from publicsuffix lookups, so the function fails early with this publicsuffix-prefixed error. validateDNS surfaces it when evaluating DNS-based scope rules.","triggerScenarios":"Calling validateDNS (via scope matching) with a hostname like \".example.com\", \"example.com.\", or \"a..b.com\" — the empty-label precheck fires and returns this error instead of attempting the publicsuffix split.","commonSituations":"URLs parsed from raw input leaving a trailing dot (FQDN form \"example.com.\"); split/join bugs building hostnames from parts (\"\" joined between labels); scope rules applied to empty or malformed Host header values; wildcard entries like \".example.com\" fed as literal hostnames.","solutions":["Normalize the hostname before scope evaluation: strip a single trailing dot and reject/trim empty labels (net.SplitHostPort for host:port, url.Parse for URLs).","Fix the upstream source of the malformed domain — log the input to find whether it's an empty Host header or a join bug.","Handle wildcard scope entries (\".example.com\") as patterns, not as hostnames passed to the DNS check.","Skip DNS validation for hosts that fail net.ParseIP/lookup and fall back to regex scope matching."],"exampleFix":"// before\nrdn, etld1, err := getDomainRDNandRDN(host) // host = \"example.com.\"\n// after\nhost = strings.TrimSuffix(strings.TrimSpace(host), \".\")\nif host == \"\" || strings.Contains(host, \"..\") {\n    return fmt.Errorf(\"invalid hostname %q\", host)\n}\nrdn, etld1, err := getDomainRDNandRDN(host)","handlingStrategy":"validation","validationCode":"func validHost(h string) bool {\n    h = strings.TrimSuffix(h, \".\")\n    return h != \"\" && !strings.HasPrefix(h, \".\") && !strings.Contains(h, \"..\")\n}","typeGuard":"func isParseableDomain(s string) bool {\n    s = strings.TrimSuffix(s, \".\")\n    return s != \"\" && strings.Contains(s, \".\") && !strings.Contains(s, \"..\")\n}","tryCatchPattern":"rdn, etld1, err := getDomainRDNandRDN(host)\nif err != nil {\n    if strings.Contains(err.Error(), \"empty label in domain\") {\n        host = strings.TrimSuffix(strings.TrimSpace(host), \".\")\n        host = strings.TrimPrefix(host, \".\")\n        rdn, etld1, err = getDomainRDNandRDN(host)\n    }\n}","preventionTips":["Always normalize hostnames (trim trailing dot, trim spaces) before scope evaluation.","Parse hosts out of URLs with url.Parse instead of manual string slicing.","Reject wildcard entries (\".example.com\") upstream so they never reach DNS validation.","Guard against empty Host headers on incoming requests before scope matching."],"tags":["dns","scope","domain-parsing"],"backgroundTag":"empty-domain-label","analyzedSha":"e3e742739c3746f085943ce918fb4e2b8daf6fe6","analyzedAt":"2026-09-03T14:55:13.248Z","contentChangedAt":"2026-09-03T14:55:13.248Z","schemaVersion":2},"datasetVersion":"2026-09-10T17:17:09.494Z"}