From 7bf4a8053a497bdd33746d181b6fa11b211c0f94 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 21 Oct 2026 09:55:04 +0200 Subject: [PATCH] Read a lowercase name with digits as a name, not a secret (issue 1439) mesh-config refused to propose mesh-issues' owners because CheckSecret took modules/asus-zephyrus-g14, i3status-rust and powerlevel10k for random strings: a run with a digit in it was judged whole. A run of lowercase letters and digits in short pieces between -, _ and / is now judged piece by piece; a capital letter or a piece of 20 or more is judged whole as before. --- go/outward/outward.go | 28 +++++- go/outward/outward_test.go | 45 +++++++++ go/outward/testdata/owners-1439.json | 137 +++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 go/outward/testdata/owners-1439.json diff --git a/go/outward/outward.go b/go/outward/outward.go index 7c28916..203e16d 100644 --- a/go/outward/outward.go +++ b/go/outward/outward.go @@ -48,6 +48,8 @@ var ( reURLCredential = regexp.MustCompile(`(?i)\b[a-z][a-z0-9+.-]*://[^/\s:@]+:[^/\s@]+@`) // reSecretRun is a run CheckSecret judges: reRun with `-`, so a base64url value is judged whole. reSecretRun = regexp.MustCompile(`[A-Za-z0-9+/=_-]{20,}`) + // reLowerName is a run written as a name is: lowercase letters, digits, and `-`, `_` or `/` between them. + reLowerName = regexp.MustCompile(`^[a-z0-9]+([-_/][a-z0-9]+)*$`) reUUID = regexp.MustCompile(`^(?i)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`) reHex = regexp.MustCompile(`(?i)\b[0-9a-f]{32,}\b`) reRun = regexp.MustCompile(`[A-Za-z0-9+/=_]{20,}`) @@ -138,8 +140,10 @@ func randomRunOutsidePaths(text string) (Refusal, bool) { pieces = strings.Split(seg, "/") } for _, piece := range pieces { - if len(piece) >= 20 && !reUUID.MatchString(piece) && looksRandomValue(piece) { - return Refusal{"secret", "a long random-looking string"}, false + for _, part := range nameParts(piece) { + if len(part) >= 20 && !reUUID.MatchString(part) && looksRandomValue(part) { + return Refusal{"secret", "a long random-looking string"}, false + } } } } @@ -147,6 +151,26 @@ func randomRunOutsidePaths(text string) (Refusal, bool) { return Refusal{}, true } +// nameParts is a run read as a name when it is one: lowercase letters and digits only, in pieces between +// `-`, `_` and `/` that are each under 20 long, as a module or repository is named (modules/asus-zephyrus-g14, +// i3status-rust: novox/hq issue 1439). Such a run is judged piece by piece, and every piece, being short, is +// a word. A run with a capital letter, or a piece of 20 or more, is judged whole, as a token's is. +func nameParts(run string) []string { + if !reLowerName.MatchString(run) { + return []string{run} + } + parts := strings.FieldsFunc(run, func(r rune) bool { return r == '-' || r == '_' || r == '/' }) + if len(parts) < 2 { + return []string{run} + } + for _, p := range parts { + if len(p) >= 20 { + return []string{run} + } + } + return parts +} + // secretShape is the secret shapes a pattern names. func secretShape(text string) (Refusal, bool) { switch { diff --git a/go/outward/outward_test.go b/go/outward/outward_test.go index 5edffba..fdc77a5 100644 --- a/go/outward/outward_test.go +++ b/go/outward/outward_test.go @@ -4,8 +4,11 @@ package outward import ( "encoding/base64" + "encoding/json" "fmt" "math/rand" + "os" + "path/filepath" "testing" ) @@ -162,3 +165,45 @@ func TestCheckSecretLetsACamelCaseNameThrough(t *testing.T) { } } } + +// 1439/AC1: a name written in lowercase words and digits between `-`, `_` or `/`, every piece short, is a +// name and not a secret: mesh-issues' owners list (the catalogue's 134 defaults plus okioki) was refused +// whole as "a long random-looking string" for modules/asus-zephyrus-g14, i3status-rust and powerlevel10k. +func TestCheckSecretLetsALowercaseNameWithDigitsThrough(t *testing.T) { + for _, text := range []string{ + "mesh-catalog modules/asus-zephyrus-g14", + "mesh-catalog modules/i3status-rust", + "mesh-catalog modules/powerlevel10k", + } { + if r, ok := CheckSecret(text); !ok { + t.Errorf("CheckSecret(%q) withheld it: %s", text, r) + } + } + raw, err := os.ReadFile(filepath.Join("testdata", "owners-1439.json")) + if err != nil { + t.Fatal(err) + } + var owners []string + if err := json.Unmarshal(raw, &owners); err != nil || len(owners) != 135 { + t.Fatalf("testdata holds %d owners (%v), want the 134 defaults plus okioki", len(owners), err) + } + compact, _ := json.Marshal(owners) + if r, ok := CheckSecret("+ owners: " + string(compact)); !ok { + t.Errorf("CheckSecret withheld the owners list: %s", r) + } +} + +// 1439/AC2: what looks random is still refused: a mixed-case run, a base64url token with `-` and `_`, a +// lowercase run of letters and digits with no separator, and a lowercase name with one long random piece. +func TestCheckSecretStillRefusesRandomRunsBesideNames(t *testing.T) { + for _, text := range []string{ + "key Zq8vR2mXk4Lp9sTw3nBy7cHd", + "token dGhpc0lz-QVNlY3JldC1_Ub2tlbl9XaXRoLUh5cGhlbnM", + "key k8s7x2m9q4w1e5r3t6y0u2p9", + "modules/k8s7x2m9q4w1e5r3t6y0u2p9-tools", + } { + if r, ok := CheckSecret(text); ok || r.Class != "secret" { + t.Errorf("CheckSecret(%q) let it through (%v)", text, r) + } + } +} diff --git a/go/outward/testdata/owners-1439.json b/go/outward/testdata/owners-1439.json new file mode 100644 index 0000000..df394f2 --- /dev/null +++ b/go/outward/testdata/owners-1439.json @@ -0,0 +1,137 @@ +[ +"hq", +"mesh-catalog", +"mesh-catalog modules/adwaita", +"mesh-catalog modules/artifact-store-tools", +"mesh-catalog modules/asus-zephyrus-g14", +"mesh-catalog modules/audit-logger", +"mesh-catalog modules/avahi", +"mesh-catalog modules/baserow", +"mesh-catalog modules/betterbird", +"mesh-catalog modules/blueman", +"mesh-catalog modules/bluetooth", +"mesh-catalog modules/ca-trust", +"mesh-catalog modules/claude-code", +"mesh-catalog modules/claude-licence-manager", +"mesh-catalog modules/clipmenu", +"mesh-catalog modules/confluence", +"mesh-catalog modules/cups", +"mesh-catalog modules/dbus", +"mesh-catalog modules/desk-channel", +"mesh-catalog modules/disk-load", +"mesh-catalog modules/distribution", +"mesh-catalog modules/dmenu", +"mesh-catalog modules/dnsmasq", +"mesh-catalog modules/docker", +"mesh-catalog modules/docker-compose", +"mesh-catalog modules/dunst", +"mesh-catalog modules/fail2ban", +"mesh-catalog modules/feh", +"mesh-catalog modules/flatpak", +"mesh-catalog modules/fonts", +"mesh-catalog modules/forticlient", +"mesh-catalog modules/gitea", +"mesh-catalog modules/gitlab", +"mesh-catalog modules/gnome-keyring", +"mesh-catalog modules/grafana", +"mesh-catalog modules/hello-web", +"mesh-catalog modules/home-assistant", +"mesh-catalog modules/hostname", +"mesh-catalog modules/i3", +"mesh-catalog modules/i3status-rust", +"mesh-catalog modules/icecast", +"mesh-catalog modules/influxdb", +"mesh-catalog modules/invoicing", +"mesh-catalog modules/jira", +"mesh-catalog modules/keycloak", +"mesh-catalog modules/lab", +"mesh-catalog modules/lemurs", +"mesh-catalog modules/letta", +"mesh-catalog modules/local-model-consumer", +"mesh-catalog modules/localization", +"mesh-catalog modules/logrotate", +"mesh-catalog modules/mailu", +"mesh-catalog modules/marrytts", +"mesh-catalog modules/matrix", +"mesh-catalog modules/memory-pressure", +"mesh-catalog modules/mesh-catalog", +"mesh-catalog modules/mesh-cli", +"mesh-catalog modules/mesh-delivery", +"mesh-catalog modules/mesh-delivery-gate", +"mesh-catalog modules/mesh-issues", +"mesh-catalog modules/mesh-vault", +"mesh-catalog modules/mesh-watcher", +"mesh-catalog modules/messenger", +"mesh-catalog modules/minio", +"mesh-catalog modules/model-usage", +"mesh-catalog modules/mongodb", +"mesh-catalog modules/mosquitto", +"mesh-catalog modules/mounts", +"mesh-catalog modules/mssql", +"mesh-catalog modules/n8n", +"mesh-catalog modules/nats", +"mesh-catalog modules/netcheck", +"mesh-catalog modules/networkmanager", +"mesh-catalog modules/nextcloud", +"mesh-catalog modules/nextcloud-client", +"mesh-catalog modules/nfs-server", +"mesh-catalog modules/nftables", +"mesh-catalog modules/nm-applet", +"mesh-catalog modules/node-env", +"mesh-catalog modules/nodered", +"mesh-catalog modules/ollama", +"mesh-catalog modules/only-office", +"mesh-catalog modules/openai-consumer", +"mesh-catalog modules/openrazer", +"mesh-catalog modules/pacman", +"mesh-catalog modules/photos-eef", +"mesh-catalog modules/photos-filip", +"mesh-catalog modules/picom", +"mesh-catalog modules/polychromatic", +"mesh-catalog modules/postgres", +"mesh-catalog modules/power", +"mesh-catalog modules/powerlevel10k", +"mesh-catalog modules/records", +"mesh-catalog modules/redis", +"mesh-catalog modules/restic", +"mesh-catalog modules/rofi", +"mesh-catalog modules/route-adapter", +"mesh-catalog modules/screen-lock", +"mesh-catalog modules/searxng", +"mesh-catalog modules/sensors", +"mesh-catalog modules/showcase", +"mesh-catalog modules/slack", +"mesh-catalog modules/snapd", +"mesh-catalog modules/ssh-client", +"mesh-catalog modules/sshd", +"mesh-catalog modules/step-ca", +"mesh-catalog modules/sudo", +"mesh-catalog modules/supabase", +"mesh-catalog modules/systemd", +"mesh-catalog modules/systemd-networkd", +"mesh-catalog modules/systemd-resolved", +"mesh-catalog modules/telegram", +"mesh-catalog modules/time-sync", +"mesh-catalog modules/triggerhappy", +"mesh-catalog modules/umami", +"mesh-catalog modules/unifi", +"mesh-catalog modules/website", +"mesh-catalog modules/xclip", +"mesh-catalog modules/xdg", +"mesh-catalog modules/xorg", +"mesh-catalog modules/xterm", +"mesh-catalog modules/zfs", +"mesh-catalog modules/zsh", +"mesh-catalog modules/zsh-autosuggestions", +"mesh-catalog modules/zsh-syntax-highlighting", +"mesh-controller", +"mesh-controller modules/build-agent", +"mesh-controller modules/route-proxy", +"mesh-delivery", +"mesh-host", +"mesh-lab", +"mesh-media-catalog", +"mesh-sdk", +"mesh-tools", +"okioki" +] \ No newline at end of file -- 2.54.0