Split cookie names on both _ and - separators

splitTokens previously picked a single separator (_ winning over -).
Cookies like __Secure-1PSID were split on _ only, leaving
"Secure-1PSID" as one token that falsely passed looksVariable,
producing the bogus ___* heuristic pattern.

The function now splits by _ first, then sub-splits each non-UUID
part by -, tracking per-gap separators. heuristicTemplate trims
leading/trailing empty tokens before analysis so that prefix
underscores are preserved in the output but do not pollute the
variable detection.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-05-18 14:20:21 +04:00
parent 0e672782ac
commit 05dbd1b476
2 changed files with 118 additions and 28 deletions

View File

@@ -349,22 +349,48 @@ func findMergeGroups(
} }
func heuristicTemplate(name string) (string, bool) { func heuristicTemplate(name string) (string, bool) {
tokens, sep := splitTokens(name) tokens, seps := splitTokens(name)
if sep == 0 { if len(seps) == 0 {
return "", false
}
// Trim leading empty tokens (e.g. "__Secure-..." yields ["", "", ...]).
var prefix string
for len(tokens) > 1 && tokens[0] == "" {
prefix += string(seps[0])
tokens = tokens[1:]
seps = seps[1:]
}
// Trim trailing empty tokens.
var suffix string
for len(tokens) > 1 && tokens[len(tokens)-1] == "" {
suffix = string(seps[len(seps)-1]) + suffix
tokens = tokens[:len(tokens)-1]
seps = seps[:len(seps)-1]
}
if len(seps) == 0 {
return "", false return "", false
} }
s := string(sep)
changed := false changed := false
var result []string var resultTokens []string
for _, t := range tokens { var resultSeps []byte
for i, t := range tokens {
if looksVariable(t) { if looksVariable(t) {
changed = true changed = true
if len(result) == 0 || result[len(result)-1] != "*" { if len(resultTokens) == 0 || resultTokens[len(resultTokens)-1] != "*" {
result = append(result, "*") if i > 0 {
resultSeps = append(resultSeps, seps[i-1])
}
resultTokens = append(resultTokens, "*")
} }
} else { } else {
result = append(result, t) if i > 0 {
resultSeps = append(resultSeps, seps[i-1])
}
resultTokens = append(resultTokens, t)
} }
} }
@@ -372,7 +398,7 @@ func heuristicTemplate(name string) (string, bool) {
return "", false return "", false
} }
return strings.Join(result, s), true return prefix + joinTokens(resultTokens, resultSeps) + suffix, true
} }
func templateCandidates(name string) []string { func templateCandidates(name string) []string {
@@ -384,11 +410,12 @@ func templateCandidates(name string) []string {
} }
} }
tokens, sep := splitTokens(name) tokens, seps := splitTokens(name)
if len(tokens) >= 3 && sep != 0 { if len(tokens) >= 3 && len(seps) > 0 {
s := string(sep)
for pos := 1; pos < len(tokens)-1; pos++ { for pos := 1; pos < len(tokens)-1; pos++ {
tmpl := strings.Join(tokens[:pos], s) + s + "*" + s + strings.Join(tokens[pos+1:], s) left := joinTokens(tokens[:pos], seps[:pos-1])
right := joinTokens(tokens[pos+1:], seps[pos+1:])
tmpl := left + string(seps[pos-1]) + "*" + string(seps[pos]) + right
candidates = append(candidates, tmpl) candidates = append(candidates, tmpl)
} }
} }
@@ -462,14 +489,45 @@ func isUUIDShape(s string) bool {
return true return true
} }
func splitTokens(name string) ([]string, byte) { func splitTokens(name string) ([]string, []byte) {
if found := strings.Contains(name, "_"); found { underscoreParts := strings.Split(name, "_")
return strings.Split(name, "_"), '_'
var tokens []string
var seps []byte
for i, part := range underscoreParts {
if i > 0 {
seps = append(seps, '_')
}
if isUUIDShape(part) || !strings.Contains(part, "-") {
tokens = append(tokens, part)
} else {
for j, sub := range strings.Split(part, "-") {
if j > 0 {
seps = append(seps, '-')
}
tokens = append(tokens, sub)
}
}
} }
if found := strings.Contains(name, "-"); found {
return strings.Split(name, "-"), '-' if len(seps) == 0 {
return []string{name}, nil
} }
return []string{name}, 0
return tokens, seps
}
func joinTokens(tokens []string, seps []byte) string {
var b strings.Builder
for i, t := range tokens {
if i > 0 {
b.WriteByte(seps[i-1])
}
b.WriteString(t)
}
return b.String()
} }
func globMatch(pattern, name string) bool { func globMatch(pattern, name string) bool {

View File

@@ -233,6 +233,11 @@ func TestHeuristicTemplate(t *testing.T) {
template: "c15t-consent-*", template: "c15t-consent-*",
changed: true, changed: true,
}, },
{
name: "leading underscores with dash not variable",
input: "__Secure-1PSID",
changed: false,
},
} }
for _, tt := range tests { for _, tt := range tests {
@@ -456,31 +461,43 @@ func TestSplitTokens(t *testing.T) {
name string name string
input string input string
tokens []string tokens []string
sep byte seps []byte
}{ }{
{ {
name: "underscore separator", name: "underscore separator",
input: "ph_phc_abc", input: "ph_phc_abc",
tokens: []string{"ph", "phc", "abc"}, tokens: []string{"ph", "phc", "abc"},
sep: '_', seps: []byte{'_', '_'},
}, },
{ {
name: "dash separator", name: "dash separator",
input: "c15t-consent-abc", input: "c15t-consent-abc",
tokens: []string{"c15t", "consent", "abc"}, tokens: []string{"c15t", "consent", "abc"},
sep: '-', seps: []byte{'-', '-'},
}, },
{ {
name: "no separator", name: "no separator",
input: "PHPSESSID", input: "PHPSESSID",
tokens: []string{"PHPSESSID"}, tokens: []string{"PHPSESSID"},
sep: 0, seps: nil,
}, },
{ {
name: "underscore takes priority over dash", name: "mixed separators split both",
input: "foo_bar-baz", input: "foo_bar-baz",
tokens: []string{"foo", "bar-baz"}, tokens: []string{"foo", "bar", "baz"},
sep: '_', seps: []byte{'_', '-'},
},
{
name: "leading underscores with dash",
input: "__Secure-1PSID",
tokens: []string{"", "", "Secure", "1PSID"},
seps: []byte{'_', '_', '-'},
},
{
name: "UUID preserved as single token",
input: "session_550e8400-e29b-41d4-a716-446655440000_data",
tokens: []string{"session", "550e8400-e29b-41d4-a716-446655440000", "data"},
seps: []byte{'_', '_'},
}, },
} }
@@ -489,9 +506,9 @@ func TestSplitTokens(t *testing.T) {
tt.name, tt.name,
func(t *testing.T) { func(t *testing.T) {
t.Parallel() t.Parallel()
tokens, sep := splitTokens(tt.input) tokens, seps := splitTokens(tt.input)
assert.Equal(t, tt.tokens, tokens) assert.Equal(t, tt.tokens, tokens)
assert.Equal(t, tt.sep, sep) assert.Equal(t, tt.seps, seps)
}, },
) )
} }
@@ -879,6 +896,21 @@ func TestFindMergeGroups(t *testing.T) {
assert.Len(t, statGroup, 3) assert.Len(t, statGroup, 3)
}, },
) )
t.Run(
"secure prefix cookies do not produce heuristic glob",
func(t *testing.T) {
t.Parallel()
patterns := coredata.TrackerPatterns{
makePattern("__Secure-1PSID", &oneYear),
makePattern("__Secure-1PSIDTS", &oneYear),
}
groups := findMergeGroups(patterns, 3)
assert.Empty(t, groups)
},
)
} }
func TestDurationBucket(t *testing.T) { func TestDurationBucket(t *testing.T) {