projectdiscovery/katana · warning

publicsuffix: empty label in domain %q

Error message

publicsuffix: empty label in domain %q

What it means

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.

Source

Thrown at pkg/utils/scope/scope.go:168

}

// matchesDomainOrSubdomain reports whether host equals domain or is one of its
// subdomains (i.e. host ends with "."+domain). Matching is case-insensitive
// because DNS labels are case-insensitive. Requiring the leading dot enforces a
// label boundary, so look-alike hosts that only share domain as a raw string
// suffix (e.g. evilexample.com vs example.com) are not considered in scope.
func matchesDomainOrSubdomain(host, domain string) bool {
	host = strings.ToLower(host)
	domain = strings.ToLower(domain)
	return host == domain || strings.HasSuffix(host, "."+domain)
}

// getDomainRDNandRDN extracts and returns the root domain name (RDN) and the
// effective top-level domain plus one label (eTLD+1) from the given hostname.
// It returns empty strings and an error if the hostname cannot be parsed.
func getDomainRDNandRDN(domain string) (string, string, error) {
	if strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") || strings.Contains(domain, "..") {
		return "", "", fmt.Errorf("publicsuffix: empty label in domain %q", domain)
	}
	suffix, _ := publicsuffix.PublicSuffix(domain)
	if len(domain) <= len(suffix) {
		return domain, "", nil
	}
	i := len(domain) - len(suffix) - 1
	if domain[i] != '.' {
		return domain, "", nil
	}
	return domain[1+strings.LastIndex(domain[:i], "."):], domain[1+strings.LastIndex(domain[:i], ".") : len(domain)-len(suffix)-1], nil
}

View on GitHub (pinned to e3e742739c)

Solutions

  1. 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).
  2. Fix the upstream source of the malformed domain — log the input to find whether it's an empty Host header or a join bug.
  3. Handle wildcard scope entries (".example.com") as patterns, not as hostnames passed to the DNS check.
  4. Skip DNS validation for hosts that fail net.ParseIP/lookup and fall back to regex scope matching.

Example fix

// before
rdn, etld1, err := getDomainRDNandRDN(host) // host = "example.com."
// after
host = strings.TrimSuffix(strings.TrimSpace(host), ".")
if host == "" || strings.Contains(host, "..") {
    return fmt.Errorf("invalid hostname %q", host)
}
rdn, etld1, err := getDomainRDNandRDN(host)
Defensive patterns

Strategy: validation

Validate before calling

func validHost(h string) bool {
    h = strings.TrimSuffix(h, ".")
    return h != "" && !strings.HasPrefix(h, ".") && !strings.Contains(h, "..")
}

Type guard

func isParseableDomain(s string) bool {
    s = strings.TrimSuffix(s, ".")
    return s != "" && strings.Contains(s, ".") && !strings.Contains(s, "..")
}

Try / catch

rdn, etld1, err := getDomainRDNandRDN(host)
if err != nil {
    if strings.Contains(err.Error(), "empty label in domain") {
        host = strings.TrimSuffix(strings.TrimSpace(host), ".")
        host = strings.TrimPrefix(host, ".")
        rdn, etld1, err = getDomainRDNandRDN(host)
    }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of projectdiscovery/katana@e3e742739c (2026-09-03). Data as JSON: /api/errors/95c28798f353475d. Report an issue: GitHub.