Skip to content

Commit 4c55fbf

Browse files
feat: make rulesengine stricter
1 parent 7851f07 commit 4c55fbf

5 files changed

Lines changed: 119 additions & 22 deletions

File tree

rulesengine/engine.go

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -76,26 +76,80 @@ func (re *Engine) matches(r Rule, method, url string) bool {
7676
}
7777

7878
if r.HostPattern != nil {
79-
// For a host pattern to match, every label has to match or be an `*`.
80-
// Subdomains also match automatically, meaning if the pattern is "example.com"
81-
// and the real is "api.example.com", it should match. We check this by comparing
82-
// from the end of the actual hostname with the pattern (which is in normal order).
79+
// Host matching is now strict:
80+
// - "github.com" matches ONLY "github.com" (exact match, no subdomains)
81+
// - "*.github.com" matches ONLY subdomains like "api.github.com" (not the base domain)
82+
// - To allow both, specify: "github.com, *.github.com"
8383

8484
labels := strings.Split(parsedUrl.Hostname(), ".")
8585

86-
// If the host pattern is longer than the actual host, it's definitely not a match
87-
if len(r.HostPattern) > len(labels) {
88-
re.logger.Debug("rule does not match", "reason", "host pattern too long", "rule", r.Raw, "method", method, "url", url, "pattern_length", len(r.HostPattern), "hostname_labels", len(labels))
89-
return false
90-
}
86+
// Special case: single "*" matches everything
87+
if len(r.HostPattern) == 1 && r.HostPattern[0] == "*" {
88+
// Matches any host
89+
} else {
90+
// Check if pattern starts with wildcard (subdomain pattern)
91+
startsWithWildcard := len(r.HostPattern) > 0 && r.HostPattern[0] == "*"
92+
93+
if startsWithWildcard {
94+
// Wildcard pattern (e.g., "*.github.com", "*.com", or "*.*")
95+
// Count how many leading wildcards we have
96+
wildcardCount := 0
97+
for i := 0; i < len(r.HostPattern) && r.HostPattern[i] == "*"; i++ {
98+
wildcardCount++
99+
}
100+
101+
if wildcardCount == len(r.HostPattern) {
102+
// Pattern is all wildcards (e.g., "*.*") - matches any domain with at least that many labels
103+
if len(labels) < len(r.HostPattern) {
104+
re.logger.Debug("rule does not match", "reason", "all-wildcard pattern requires at least pattern length labels", "rule", r.Raw, "method", method, "url", url, "pattern_length", len(r.HostPattern), "hostname_labels", len(labels))
105+
return false
106+
}
107+
} else if len(r.HostPattern) == 2 {
108+
// Pattern like "*.com" - matches any single-label domain (same length)
109+
if len(labels) != len(r.HostPattern) {
110+
re.logger.Debug("rule does not match", "reason", "wildcard pattern requires same length for 2-label patterns", "rule", r.Raw, "method", method, "url", url, "pattern_length", len(r.HostPattern), "hostname_labels", len(labels))
111+
return false
112+
}
113+
} else {
114+
// Pattern like "*.github.com" (3+ labels) - requires subdomain
115+
// Host must have >= pattern length, but not be the base domain
116+
if len(labels) < len(r.HostPattern) {
117+
re.logger.Debug("rule does not match", "reason", "wildcard pattern requires subdomain but host has fewer labels", "rule", r.Raw, "method", method, "url", url, "pattern_length", len(r.HostPattern), "hostname_labels", len(labels))
118+
return false
119+
}
120+
// Also ensure it's not the base domain (which would have len(labels) == len(r.HostPattern) - 1)
121+
if len(labels) == len(r.HostPattern)-1 {
122+
re.logger.Debug("rule does not match", "reason", "wildcard pattern does not match base domain", "rule", r.Raw, "method", method, "url", url, "pattern_length", len(r.HostPattern), "hostname_labels", len(labels))
123+
return false
124+
}
125+
}
91126

92-
// Since host patterns cannot end with asterisk, we only need to handle:
93-
// "example.com" or "*.example.com" - match from the end (allowing subdomains)
94-
for i, lp := range r.HostPattern {
95-
labelIndex := len(labels) - len(r.HostPattern) + i
96-
if string(lp) != labels[labelIndex] && lp != "*" {
97-
re.logger.Debug("rule does not match", "reason", "host pattern label mismatch", "rule", r.Raw, "method", method, "url", url, "expected", string(lp), "actual", labels[labelIndex])
98-
return false
127+
// Match from the end (excluding the wildcard at the start)
128+
// Pattern: ["*", "github", "com"]
129+
// Host: ["api", "github", "com"] -> match "github" and "com"
130+
for i := 1; i < len(r.HostPattern); i++ {
131+
lp := r.HostPattern[i]
132+
labelIndex := len(labels) - len(r.HostPattern) + i
133+
if string(lp) != labels[labelIndex] && lp != "*" {
134+
re.logger.Debug("rule does not match", "reason", "host pattern label mismatch", "rule", r.Raw, "method", method, "url", url, "expected", string(lp), "actual", labels[labelIndex])
135+
return false
136+
}
137+
}
138+
} else {
139+
// Exact domain pattern (e.g., "github.com")
140+
// Must match exactly - same length, all labels match
141+
if len(r.HostPattern) != len(labels) {
142+
re.logger.Debug("rule does not match", "reason", "exact domain pattern requires exact length match", "rule", r.Raw, "method", method, "url", url, "pattern_length", len(r.HostPattern), "hostname_labels", len(labels))
143+
return false
144+
}
145+
146+
// All labels must match exactly
147+
for i, lp := range r.HostPattern {
148+
if string(lp) != labels[i] && lp != "*" {
149+
re.logger.Debug("rule does not match", "reason", "host pattern label mismatch", "rule", r.Raw, "method", method, "url", url, "expected", string(lp), "actual", labels[i])
150+
return false
151+
}
152+
}
99153
}
100154
}
101155
}

rulesengine/engine_test.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,32 @@ func TestEngineMatches(t *testing.T) {
7474
expected: false,
7575
},
7676
{
77-
name: "subdomain matches",
77+
name: "subdomain does not match exact domain pattern",
7878
rule: Rule{
7979
HostPattern: []string{"example", "com"},
8080
},
8181
method: "GET",
8282
url: "https://api.example.com/users",
83+
expected: false,
84+
},
85+
{
86+
name: "wildcard subdomain pattern matches subdomain",
87+
rule: Rule{
88+
HostPattern: []string{"*", "example", "com"},
89+
},
90+
method: "GET",
91+
url: "https://api.example.com/users",
8392
expected: true,
8493
},
94+
{
95+
name: "wildcard subdomain pattern does not match base domain",
96+
rule: Rule{
97+
HostPattern: []string{"*", "example", "com"},
98+
},
99+
method: "GET",
100+
url: "https://example.com/users",
101+
expected: false,
102+
},
85103
{
86104
name: "host pattern too long",
87105
rule: Rule{

rulesengine/parse_and_match_test.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,37 @@ func TestRoundTrip(t *testing.T) {
9898
expectMatch: true,
9999
},
100100
{
101-
name: "domain wildcard segment matches",
101+
name: "domain wildcard segment matches subdomain",
102102
rules: []string{"domain=*.github.com"},
103103
url: "https://api.github.com/repos",
104104
method: "GET",
105105
expectParse: true,
106106
expectMatch: true,
107107
},
108+
{
109+
name: "domain wildcard does not match base domain",
110+
rules: []string{"domain=*.github.com"},
111+
url: "https://github.com/repos",
112+
method: "GET",
113+
expectParse: true,
114+
expectMatch: false,
115+
},
116+
{
117+
name: "exact domain does not match subdomain",
118+
rules: []string{"domain=github.com"},
119+
url: "https://api.github.com/repos",
120+
method: "GET",
121+
expectParse: true,
122+
expectMatch: false,
123+
},
124+
{
125+
name: "exact domain matches only exact domain",
126+
rules: []string{"domain=github.com"},
127+
url: "https://github.com/repos",
128+
method: "GET",
129+
expectParse: true,
130+
expectMatch: true,
131+
},
108132
{
109133
name: "domain cannot end with asterisk",
110134
rules: []string{"domain=github.*"},
@@ -281,12 +305,12 @@ func TestRoundTripExtraRules(t *testing.T) {
281305
expectMatch: false,
282306
},
283307
{
284-
name: "includes all subdomains by default",
308+
name: "exact domain does not match subdomains",
285309
rules: []string{"domain=github.com"},
286310
url: "https://x.users.api.github.com",
287311
method: "GET",
288312
expectParse: true,
289-
expectMatch: true,
313+
expectMatch: false,
290314
},
291315
{
292316
name: "domain wildcard in the middle matches exactly one label",

rulesengine/rules.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ type Rule struct {
2020
// The labels of the host, i.e. ["google", "com"].
2121
// - nil means all hosts allowed
2222
// - A label of `*` acts as a wild card.
23-
// - subdomains automatically match
23+
// - Exact domain patterns (e.g., "github.com") match ONLY the exact domain (no subdomains)
24+
// - Wildcard patterns starting with "*" (e.g., "*.github.com") match ONLY subdomains (not the base domain)
2425
HostPattern []string
2526

2627
// The allowed http methods.

rulesengine/rules_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1103,7 +1103,7 @@ func TestReadmeExamples(t *testing.T) {
11031103
}{
11041104
{"GET", "https://github.com", true},
11051105
{"POST", "https://github.com/user/repo", true},
1106-
{"GET", "https://api.github.com", true}, // subdomain match
1106+
{"GET", "https://api.github.com", false}, // subdomain does not match exact domain
11071107
{"GET", "https://example.com", false},
11081108
},
11091109
},

0 commit comments

Comments
 (0)