From 05dbd1b476b27457398016bf3184644f5734c87e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Mon, 18 May 2026 14:20:21 +0400 Subject: [PATCH] Split cookie names on both _ and - separators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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é --- pkg/cookiebanner/pattern_analysis_worker.go | 96 +++++++++++++++---- .../pattern_analysis_worker_test.go | 50 ++++++++-- 2 files changed, 118 insertions(+), 28 deletions(-) diff --git a/pkg/cookiebanner/pattern_analysis_worker.go b/pkg/cookiebanner/pattern_analysis_worker.go index 8fd09ee4c..43cd1520a 100644 --- a/pkg/cookiebanner/pattern_analysis_worker.go +++ b/pkg/cookiebanner/pattern_analysis_worker.go @@ -349,22 +349,48 @@ func findMergeGroups( } func heuristicTemplate(name string) (string, bool) { - tokens, sep := splitTokens(name) - if sep == 0 { + tokens, seps := splitTokens(name) + 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 } - s := string(sep) changed := false - var result []string - for _, t := range tokens { + var resultTokens []string + var resultSeps []byte + for i, t := range tokens { if looksVariable(t) { changed = true - if len(result) == 0 || result[len(result)-1] != "*" { - result = append(result, "*") + if len(resultTokens) == 0 || resultTokens[len(resultTokens)-1] != "*" { + if i > 0 { + resultSeps = append(resultSeps, seps[i-1]) + } + resultTokens = append(resultTokens, "*") } } 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 strings.Join(result, s), true + return prefix + joinTokens(resultTokens, resultSeps) + suffix, true } func templateCandidates(name string) []string { @@ -384,11 +410,12 @@ func templateCandidates(name string) []string { } } - tokens, sep := splitTokens(name) - if len(tokens) >= 3 && sep != 0 { - s := string(sep) + tokens, seps := splitTokens(name) + if len(tokens) >= 3 && len(seps) > 0 { 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) } } @@ -462,14 +489,45 @@ func isUUIDShape(s string) bool { return true } -func splitTokens(name string) ([]string, byte) { - if found := strings.Contains(name, "_"); found { - return strings.Split(name, "_"), '_' +func splitTokens(name string) ([]string, []byte) { + underscoreParts := 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 { diff --git a/pkg/cookiebanner/pattern_analysis_worker_test.go b/pkg/cookiebanner/pattern_analysis_worker_test.go index d275218fd..eeeba240d 100644 --- a/pkg/cookiebanner/pattern_analysis_worker_test.go +++ b/pkg/cookiebanner/pattern_analysis_worker_test.go @@ -233,6 +233,11 @@ func TestHeuristicTemplate(t *testing.T) { template: "c15t-consent-*", changed: true, }, + { + name: "leading underscores with dash not variable", + input: "__Secure-1PSID", + changed: false, + }, } for _, tt := range tests { @@ -456,31 +461,43 @@ func TestSplitTokens(t *testing.T) { name string input string tokens []string - sep byte + seps []byte }{ { name: "underscore separator", input: "ph_phc_abc", tokens: []string{"ph", "phc", "abc"}, - sep: '_', + seps: []byte{'_', '_'}, }, { name: "dash separator", input: "c15t-consent-abc", tokens: []string{"c15t", "consent", "abc"}, - sep: '-', + seps: []byte{'-', '-'}, }, { name: "no separator", input: "PHPSESSID", tokens: []string{"PHPSESSID"}, - sep: 0, + seps: nil, }, { - name: "underscore takes priority over dash", + name: "mixed separators split both", input: "foo_bar-baz", - tokens: []string{"foo", "bar-baz"}, - sep: '_', + tokens: []string{"foo", "bar", "baz"}, + 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, func(t *testing.T) { t.Parallel() - tokens, sep := splitTokens(tt.input) + tokens, seps := splitTokens(tt.input) 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) }, ) + + 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) {