Read a lowercase name with digits as a name, not a secret (issue 1439)
mesh/repo-check head 7bf4a8053a49 merged into trunk 36d6f7745c8d — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
mesh/merge-gate head 7bf4a8053a49 merged into trunk 36d6f7745c8d — pass: group feat/1439-name-runs composed: every machine composes with the change as it d…
mesh/delivery delivered
mesh/delivery-group group feat/1439-name-runs delivered: every member is delivered

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.
This commit is contained in:
jochen
2026-10-21 09:55:04 +02:00
parent 36d6f7745c
commit 7bf4a8053a
3 changed files with 208 additions and 2 deletions
+26 -2
View File
@@ -48,6 +48,8 @@ var (
reURLCredential = regexp.MustCompile(`(?i)\b[a-z][a-z0-9+.-]*://[^/\s:@]+:[^/\s@]+@`) 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 is a run CheckSecret judges: reRun with `-`, so a base64url value is judged whole.
reSecretRun = regexp.MustCompile(`[A-Za-z0-9+/=_-]{20,}`) 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}$`) 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`) reHex = regexp.MustCompile(`(?i)\b[0-9a-f]{32,}\b`)
reRun = regexp.MustCompile(`[A-Za-z0-9+/=_]{20,}`) reRun = regexp.MustCompile(`[A-Za-z0-9+/=_]{20,}`)
@@ -138,8 +140,10 @@ func randomRunOutsidePaths(text string) (Refusal, bool) {
pieces = strings.Split(seg, "/") pieces = strings.Split(seg, "/")
} }
for _, piece := range pieces { for _, piece := range pieces {
if len(piece) >= 20 && !reUUID.MatchString(piece) && looksRandomValue(piece) { for _, part := range nameParts(piece) {
return Refusal{"secret", "a long random-looking string"}, false 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 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. // secretShape is the secret shapes a pattern names.
func secretShape(text string) (Refusal, bool) { func secretShape(text string) (Refusal, bool) {
switch { switch {
+45
View File
@@ -4,8 +4,11 @@ package outward
import ( import (
"encoding/base64" "encoding/base64"
"encoding/json"
"fmt" "fmt"
"math/rand" "math/rand"
"os"
"path/filepath"
"testing" "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)
}
}
}
+137
View File
@@ -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"
]