From 6647cb2e05e88fa35595c26c0c51a4584def1fe1 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 31 Oct 2026 14:18:59 +0000 Subject: [PATCH] policy/v2: map no numeric email local part to a login The client looks a numeric login up as a UID, so 0@domain reached root under a localpart rule, and another number someone else's account. --- hscontrol/policy/v2/filter.go | 8 ++++++++ hscontrol/policy/v2/filter_test.go | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/hscontrol/policy/v2/filter.go b/hscontrol/policy/v2/filter.go index 3f9902d91..9393781bd 100644 --- a/hscontrol/policy/v2/filter.go +++ b/hscontrol/policy/v2/filter.go @@ -5,6 +5,7 @@ import ( "fmt" "net/netip" "slices" + "strconv" "strings" "sync" "time" @@ -659,6 +660,13 @@ func resolveLocalparts( continue } + // The client looks a numeric login up as a UID, so 0@domain would + // reach root and another number someone else's account. + _, notNumeric := strconv.ParseUint(localpart, util.Base10, 32) + if notNumeric == nil { + continue + } + result[user.ID] = localpart } } diff --git a/hscontrol/policy/v2/filter_test.go b/hscontrol/policy/v2/filter_test.go index 0eae6965d..2880dff7d 100644 --- a/hscontrol/policy/v2/filter_test.go +++ b/hscontrol/policy/v2/filter_test.go @@ -3112,6 +3112,17 @@ func TestResolveLocalparts(t *testing.T) { }, want: map[uint]string{1: "dave+ssh"}, }, + { + name: "numeric local part is not a login", + entries: []SSHUser{"localpart:*@example.com"}, + users: types.Users{ + {Name: "zero", Email: "0@example.com", ID: 1}, + {Name: "zeros", Email: "00@example.com", ID: 2}, + {Name: "employee", Email: "1042@example.com", ID: 3}, + {Name: "mixed", Email: "1042a@example.com", ID: 4}, + }, + want: map[uint]string{4: "1042a"}, + }, } for _, tt := range tests {