diff --git a/cmd/datdump/main.go b/cmd/datdump/main.go index 518c5d8a..7caee04f 100644 --- a/cmd/datdump/main.go +++ b/cmd/datdump/main.go @@ -83,7 +83,7 @@ func exportSite(name string, gs *GeoSites) error { } defer file.Close() w := bufio.NewWriter(file) - fmt.Fprintf(w, "%s:\n", name) + fmt.Fprintf(w, "%q:\n", name) var b strings.Builder b.Grow(64) for _, vdomain := range vDomains { diff --git a/main.go b/main.go index 39a95571..6dd11577 100644 --- a/main.go +++ b/main.go @@ -66,6 +66,9 @@ const ( ModeAll string = "all" ModeAllowlist string = "allowlist" ModeDenylist string = "denylist" + + maxDomainLen int = 253 // Maximum length of a domain name + maxLabelLen int = 63 // Maximum length of a label of a domain name ) func makeProtoList(listName string, entries []*Entry) *router.GeoSite { @@ -208,10 +211,15 @@ func parseEntry(typ, rule string) (*Entry, []string, error) { return entry, nil, fmt.Errorf("invalid regexp %q: %w", parts[0], err) } entry.Value = parts[0] - case dlc.RuleTypeDomain, dlc.RuleTypeFullDomain, dlc.RuleTypeKeyword: + case dlc.RuleTypeDomain, dlc.RuleTypeFullDomain: + entry.Value = strings.ToLower(parts[0]) + if !validateDomainName(entry.Value) { + return entry, nil, fmt.Errorf("invalid domain: %q", entry.Value) + } + case dlc.RuleTypeKeyword: entry.Value = strings.ToLower(parts[0]) if !validateDomainChars(entry.Value) { - return entry, nil, fmt.Errorf("invalid domain: %q", entry.Value) + return entry, nil, fmt.Errorf("invalid keyword: %q", entry.Value) } default: return entry, nil, fmt.Errorf("unknown rule type: %q", entry.Type) @@ -310,6 +318,21 @@ func validateDomainChars(domain string) bool { return true } +// validateDomainName reports whether the domain is a valid domain name, so that +// typos like "example..com" or "-example.com" would not be silently built into +// rules which can never match any domain. +func validateDomainName(domain string) bool { + if !validateDomainChars(domain) || len(domain) > maxDomainLen { + return false + } + for label := range strings.SplitSeq(domain, ".") { + if label == "" || len(label) > maxLabelLen || label[0] == '-' || label[len(label)-1] == '-' { + return false + } + } + return true +} + func validateAttrChars(attr string) bool { if attr == "" { return false diff --git a/main_test.go b/main_test.go index a7eb5cfa..55e293f7 100644 --- a/main_test.go +++ b/main_test.go @@ -22,10 +22,19 @@ func TestParseEntry(t *testing.T) { {name: "duplicated attrs", typ: "domain", rule: "example.com @ads @ads", wantPlain: "domain:example.com:@ads"}, {name: "affiliations", typ: "domain", rule: "example.com &other @ads", wantPlain: "domain:example.com:@ads", wantAffs: []string{"OTHER"}}, {name: "regexp", typ: "regexp", rule: `^example\.com$`, wantPlain: `regexp:^example\.com$`}, + {name: "tld", typ: "domain", rule: "google", wantPlain: "domain:google"}, + {name: "hyphen inside", typ: "domain", rule: "my-example.com", wantPlain: "domain:my-example.com"}, + {name: "keyword allows partial", typ: "keyword", rule: "-ads-", wantPlain: "keyword:-ads-"}, {name: "invalid regexp", typ: "regexp", rule: "^example(", wantErr: true}, {name: "empty rule", typ: "domain", rule: " ", wantErr: true}, {name: "unknown type", typ: "prefix", rule: "example.com", wantErr: true}, {name: "invalid domain", typ: "domain", rule: "exa_mple.com @ads", wantErr: true}, + {name: "empty label", typ: "domain", rule: "example..com", wantErr: true}, + {name: "trailing dot", typ: "full", rule: "example.com.", wantErr: true}, + {name: "leading dot", typ: "domain", rule: ".example.com", wantErr: true}, + {name: "leading hyphen", typ: "domain", rule: "-example.com", wantErr: true}, + {name: "trailing hyphen", typ: "full", rule: "example-.com", wantErr: true}, + {name: "overlong label", typ: "domain", rule: strings.Repeat("a", 64) + ".com", wantErr: true}, {name: "empty attr", typ: "domain", rule: "example.com @", wantErr: true}, {name: "empty affiliation", typ: "domain", rule: "example.com &", wantErr: true}, {name: "unknown field", typ: "domain", rule: "example.com ads", wantErr: true},