From 6e8df2fa2edc68ae2ae001ec394622456f5971b5 Mon Sep 17 00:00:00 2001 From: Ted Robertson <10043369+tredondo@users.noreply.github.com> Date: Thu, 21 May 2026 04:13:29 -0700 Subject: [PATCH] daemon/rule: stop mutating regex Data field on Compile (#1587) Compile() for Regexp operators with Sensitive==false used to do o.Data = strings.ToLower(o.Data) before compiling the pattern. That works as an ASCII-only way to make matching case-insensitive (pattern and subject both lowercased), but it mangles the pattern string itself. A regex like [0-9A-Za-z]+ becomes [0-9a-za-z]+: still a valid character class that matches the same set of characters, but visibly wrong, and the mangled value then bleeds into: - the rule JSON written to disk by loader.Save (json.MarshalIndent serializes the post-Compile o.Data) - the GUI display of the operator data and the slugified rule name This is most visible on the auto-generated rules for AppImages, whose process.path lives under /tmp/.mount__/, producing the [0-9a-za-z]+ duplication the user can see in the popup and on disk. Switch to Go regexp's native case-insensitivity instead: prepend the (?i) flag to a local pattern string and compile that. o.Data is no longer touched by Compile, so the original pattern survives both serialization and the round-trip back to the UI. reCmp can drop its matching strings.ToLower(data) call, since the compiled regex now handles case folding itself. Existing rules on disk that already have a lowercased pattern keep working: (?i) over a [0-9a-z]+-style class is equivalent to the previous lowercase-both-sides behavior, and case-sensitive rules are unaffected (no (?i) is added). A regression test (TestNewOperatorRegexpDataNotMutated) pins both the pattern-preservation contract and case-insensitive matching against an appimage-style path. Fixes #1587 --- daemon/rule/operator.go | 15 +++++++++----- daemon/rule/operator_test.go | 38 ++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/daemon/rule/operator.go b/daemon/rule/operator.go index 4bce88bf86..7a4282356d 100644 --- a/daemon/rule/operator.go +++ b/daemon/rule/operator.go @@ -172,10 +172,16 @@ func (o *Operator) Compile() error { o.cb = o.rangeCmp } else if o.Type == Regexp { o.cb = o.reCmp + // Mark the compiled regex as case-insensitive via Go's (?i) flag + // instead of lowercasing o.Data. Lowercasing the pattern itself + // mangles character classes like [0-9A-Za-z] into [0-9a-za-z], + // which then leaks into the rule's saved JSON and the GUI. + // See https://github.com/evilsocket/opensnitch/issues/1587. + pattern := o.Data if o.Sensitive == false { - o.Data = strings.ToLower(o.Data) + pattern = "(?i)" + pattern } - re, err := regexp.Compile(o.Data) + re, err := regexp.Compile(pattern) if err != nil { return err } @@ -289,9 +295,8 @@ func (o *Operator) simpleCmp(v string) bool { } func (o *Operator) reCmp(data string) bool { - if o.Sensitive == false { - data = strings.ToLower(data) - } + // Case folding for the insensitive case is baked into the compiled regex + // via the (?i) flag added in Compile(), so the subject is matched as-is. return o.re.MatchString(data) } diff --git a/daemon/rule/operator_test.go b/daemon/rule/operator_test.go index 62509d4a3f..9de06f767a 100644 --- a/daemon/rule/operator_test.go +++ b/daemon/rule/operator_test.go @@ -768,6 +768,44 @@ func TestNewOperatorRegexpSensitive(t *testing.T) { restoreConnection() } +// TestNewOperatorRegexpDataNotMutated guards against regressions of issue +// #1587: when Sensitive is false (the default), Compile() must not mutate +// Operator.Data. Lowercasing the pattern in place mangles character classes +// like [0-9A-Za-z] into [0-9a-za-z] and bleeds the mangled regex back into +// the rule's saved JSON and the GUI. +func TestNewOperatorRegexpDataNotMutated(t *testing.T) { + t.Log("Test NewOperator() regexp data preservation (#1587)") + var dummyList []Operator + + appimagePattern := `^/tmp/\.mount_handy_[0-9A-Za-z]+\/.*handy$` + opRE, err := NewOperator(Regexp, false, OpProcessPath, appimagePattern, dummyList) + if err != nil { + t.Fatalf("NewOperator regexp.err should be nil: %s", err) + } + if err = opRE.Compile(); err != nil { + t.Fatalf("Compile() should not fail: %s", err) + } + if opRE.Data != appimagePattern { + t.Errorf("Operator.Data was mutated by Compile():\n want: %q\n got: %q", appimagePattern, opRE.Data) + } + + t.Run("matches mixed-case appimage path case-insensitively", func(t *testing.T) { + conn.Process.Path = "/tmp/.mount_handy_aB3xZ9/handy" + if opRE.Match(conn, false) == false { + t.Errorf("regexp should match mixed-case path: %s", conn.Process.Path) + } + }) + + t.Run("matches lowercase appimage path", func(t *testing.T) { + conn.Process.Path = "/tmp/.mount_handy_abcdef/handy" + if opRE.Match(conn, false) == false { + t.Errorf("regexp should match lowercase path: %s", conn.Process.Path) + } + }) + + restoreConnection() +} + func TestNewOperatorList(t *testing.T) { t.Log("Test NewOperator() List") var list []Operator