From 7122f74ebfc859cf983a86b8354edb0e38241852 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 12 Oct 2026 19:47:31 +0200 Subject: [PATCH] Read an account by its name, never as a number getent takes for a uid (hq ADR 0325) getent passwd reads an all-digit key as a uid, so a tool runner's account named "1000" resolved to whoever has uid 1000 and the entry went to them. Only an answer naming the account asked for is taken. --- internal/accounts/exec.go | 6 ++++++ internal/accounts/ids_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 internal/accounts/ids_test.go diff --git a/internal/accounts/exec.go b/internal/accounts/exec.go index 277f6fa..15b42d3 100644 --- a/internal/accounts/exec.go +++ b/internal/accounts/exec.go @@ -301,6 +301,12 @@ func (e Exec) IDs(ctx context.Context, account string) (uid, gid int, found bool return 0, 0, false, fmt.Errorf("the user database gave %q for %q, which is not a passwd entry", strings.TrimSpace(out), account) } + // getent reads an all-digit key as a uid: "1000" answers with whoever has uid 1000. Only the account named is + // the one asked for. + if fields[0] != account { + return 0, 0, false, fmt.Errorf("the user database answered %q with the account %q: an account is named, "+ + "never numbered", account, fields[0]) + } uid, uerr := strconv.Atoi(fields[2]) gid, gerr := strconv.Atoi(fields[3]) if uerr != nil || gerr != nil { diff --git a/internal/accounts/ids_test.go b/internal/accounts/ids_test.go new file mode 100644 index 0000000..fdd9f85 --- /dev/null +++ b/internal/accounts/ids_test.go @@ -0,0 +1,32 @@ +package accounts + +import ( + "context" + "errors" + "strings" + "testing" +) + +// An account is read by its name (novox/hq ADR 0325 §4): getent reads an all-digit key as a uid, so "1000" would +// answer with whoever has uid 1000 — refused, never taken for the account named. +func TestAnAccountIsReadByItsNameNeverItsNumber(t *testing.T) { + db := map[string]string{ + "nox": "nox:x:1003:1003::/home/nox:/bin/sh", + "1000": "jochen:x:1000:1000::/home/jochen:/bin/zsh", // what getent answers for a uid + } + e := Exec{Run: func(_ context.Context, name string, args ...string) (string, error) { + if line, ok := db[args[len(args)-1]]; ok { + return line + "\n", nil + } + return "", errors.New("getent exited 2") + }} + if uid, gid, found, err := e.IDs(context.Background(), "nox"); err != nil || !found || uid != 1003 || gid != 1003 { + t.Fatalf("nox: %d %d %v %v", uid, gid, found, err) + } + if _, _, found, err := e.IDs(context.Background(), "1000"); found || err == nil || !strings.Contains(err.Error(), "jochen") { + t.Fatalf("an all-digit name was read as a uid: found=%v err=%v", found, err) + } + if _, _, found, err := e.IDs(context.Background(), "ghost"); found || err != nil { + t.Fatalf("an absent account: found=%v err=%v", found, err) + } +}