diff --git a/internal/tools/shell_deny_groups.go b/internal/tools/shell_deny_groups.go index d2adce35..7ebb9adc 100644 --- a/internal/tools/shell_deny_groups.go +++ b/internal/tools/shell_deny_groups.go @@ -4,6 +4,17 @@ import ( "regexp" ) +// unicodeCmdWord matches a command name (or alternation) as a standalone +// word using a Unicode-aware boundary. Go's RE2 \b is ASCII-only, so a +// pattern like \bsu\b matches the "su" inside non-ASCII words such as the +// Vietnamese "suất" (the accented 'ấ' is not an ASCII word char, so \b +// sees a spurious boundary right after "su"). Treating Unicode letters and +// digits (\pL, \pN) as word chars avoids those false positives while still +// matching the real `su` command. +func unicodeCmdWord(alt string) *regexp.Regexp { + return regexp.MustCompile(`(?:^|[^\pL\pN_])(?:` + alt + `)(?:[^\pL\pN_]|$)`) +} + // DenyGroup is a named set of shell command deny patterns. type DenyGroup struct { Name string // machine name: "package_install" @@ -83,7 +94,7 @@ var DenyGroupRegistry = map[string]*DenyGroup{ Default: true, Patterns: []*regexp.Regexp{ regexp.MustCompile(`\bsudo\b`), - regexp.MustCompile(`\bsu\b`), + unicodeCmdWord("su"), regexp.MustCompile(`\bdoas\b`), regexp.MustCompile(`\bpkexec\b`), regexp.MustCompile(`\brunuser\b`), diff --git a/internal/tools/shell_deny_test.go b/internal/tools/shell_deny_test.go index 7017875e..8ddac91d 100644 --- a/internal/tools/shell_deny_test.go +++ b/internal/tools/shell_deny_test.go @@ -130,14 +130,19 @@ func TestPrivilegeEscalationGaps(t *testing.T) { ) mustAllow(t, patterns, - "summit", // not "su" - "sugar", // not "su" - "surplus", // not "su" - "issue", // not "su" - "result", // not "su" - "resume", // not "su" - "visual", // not "su" - "sushi", // not "su" + "summit", // not "su" + "sugar", // not "su" + "surplus", // not "su" + "issue", // not "su" + "result", // not "su" + "resume", // not "su" + "visual", // not "su" + "sushi", // not "su" + // non-ASCII (Vietnamese) text: ASCII "su" adjacent to an accented + // vowel must NOT match the su command (Go RE2 word boundaries are ASCII-only). + "hiệu suất", + "năng suất", + "báo cáo hiệu suất tháng", "doaspkg", // not "doas" (no word boundary) "pkexecute", // not "pkexec" (no word boundary) )