Moderate severityNVD Advisory· Published Jun 16, 2025· Updated Jun 17, 2025
Improper Permission Management in SSH Session Handling
CVE-2025-5689
Description
A flaw was found in the temporary user record that authd uses in the pre-auth NSS. As a result, a user login for the first time will be considered to be part of the root group in the context of that SSH session.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/ubuntu/authdGo | < 0.5.4 | 0.5.4 |
Affected products
1- Range: 0.0.0
Patches
1619ce8e55953Fix new users logging in via SSH being part of the root group
35 files changed · +102 −71
internal/services/pam/pam_test.go+2 −2 modified@@ -466,7 +466,7 @@ func TestIsAuthenticated(t *testing.T) { managerOpts := []users.Option{ users.WithIDGenerator(&idgenerator.IDGeneratorMock{ UIDsToGenerate: []uint32{1111}, - GIDsToGenerate: []uint32{1111, 2222}, + GIDsToGenerate: []uint32{22222}, }), } @@ -560,7 +560,7 @@ func TestIDGeneration(t *testing.T) { managerOpts := []users.Option{ users.WithIDGenerator(&idgenerator.IDGeneratorMock{ UIDsToGenerate: []uint32{1111}, - GIDsToGenerate: []uint32{1111, 2222}, + GIDsToGenerate: []uint32{22222}, }), }
internal/services/pam/testdata/golden/TestIDGeneration/Generate_ID/cache.db+2 −2 modified@@ -10,11 +10,11 @@ groups: gid: 1111 ugid: testidgeneration_separator_success - name: group-success - gid: 2222 + gid: 22222 ugid: ugid-success users_to_groups: - uid: 1111 gid: 1111 - uid: 1111 - gid: 2222 + gid: 22222 schema_version: 1
internal/services/pam/testdata/golden/TestIsAuthenticated/Error_on_updating_local_groups_with_unexisting_file/cache.db+2 −2 modified@@ -10,11 +10,11 @@ groups: gid: 1111 ugid: testisauthenticated/error_on_updating_local_groups_with_unexisting_file_separator_success_with_local_groups - name: group-success_with_local_groups - gid: 2222 + gid: 22222 ugid: ugid-success_with_local_groups users_to_groups: - uid: 1111 gid: 1111 - uid: 1111 - gid: 2222 + gid: 22222 schema_version: 1
internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_calling_second_time_without_cancelling/cache.db+2 −2 modified@@ -10,11 +10,11 @@ groups: gid: 1111 ugid: testisauthenticated/error_when_calling_second_time_without_cancelling_separator_ia_second_call - name: group-ia_second_call - gid: 2222 + gid: 22222 ugid: ugid-ia_second_call users_to_groups: - uid: 1111 gid: 1111 - uid: 1111 - gid: 2222 + gid: 22222 schema_version: 1
internal/services/pam/testdata/golden/TestIsAuthenticated/Successfully_authenticate/cache.db+2 −2 modified@@ -10,11 +10,11 @@ groups: gid: 1111 ugid: testisauthenticated/successfully_authenticate_separator_success - name: group-success - gid: 2222 + gid: 22222 ugid: ugid-success users_to_groups: - uid: 1111 gid: 1111 - uid: 1111 - gid: 2222 + gid: 22222 schema_version: 1
internal/services/pam/testdata/golden/TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled/cache.db+2 −2 modified@@ -10,11 +10,11 @@ groups: gid: 1111 ugid: testisauthenticated/successfully_authenticate_if_first_call_is_canceled_separator_ia_second_call - name: group-ia_second_call - gid: 2222 + gid: 22222 ugid: ugid-ia_second_call users_to_groups: - uid: 1111 gid: 1111 - uid: 1111 - gid: 2222 + gid: 22222 schema_version: 1
internal/services/pam/testdata/golden/TestIsAuthenticated/Update_local_groups/cache.db+2 −2 modified@@ -10,11 +10,11 @@ groups: gid: 1111 ugid: testisauthenticated/update_local_groups_separator_success_with_local_groups - name: group-success_with_local_groups - gid: 2222 + gid: 22222 ugid: ugid-success_with_local_groups users_to_groups: - uid: 1111 gid: 1111 - uid: 1111 - gid: 2222 + gid: 22222 schema_version: 1
internal/services/user/testdata/golden/TestGetUserByName/Prechecked_user_with_upper_cases_in_username_has_same_id_as_lower_case+1 −1 modified@@ -1,6 +1,6 @@ name: user-pre-check uid: 1234 -gid: 0 +gid: 1234 gecos: gecos for user-pre-check homedir: /home/user-pre-check shell: /bin/sh/user-pre-check
internal/services/user/testdata/golden/TestGetUserByName/Precheck_user_if_not_in_db+1 −1 modified@@ -1,6 +1,6 @@ name: user-pre-check uid: 1234 -gid: 0 +gid: 1234 gecos: gecos for user-pre-check homedir: /home/user-pre-check shell: /bin/sh/user-pre-check
internal/services/user/user.go+2 −0 modified@@ -194,6 +194,8 @@ func (s Service) userPreCheck(ctx context.Context, username string) (types.UserE if err != nil { return types.UserEntry{}, fmt.Errorf("failed to add temporary record for user %q: %v", username, err) } + // The UID is also the GID of the user private group (see https://wiki.debian.org/UserPrivateGroups#UPGs) + u.GID = u.UID return u, nil }
internal/services/user/user_test.go+0 −1 modified@@ -290,7 +290,6 @@ func newUserManagerForTests(t *testing.T, dbFile string) *users.Manager { managerOpts := []users.Option{ users.WithIDGenerator(&idgenerator.IDGeneratorMock{ UIDsToGenerate: []uint32{1234}, - GIDsToGenerate: []uint32{1234}, }), }
internal/users/manager.go+7 −6 modified@@ -160,7 +160,7 @@ func (m *Manager) UpdateUser(u types.UserInfo) (err error) { } // Prepend the user private group - u.Groups = append([]types.GroupInfo{{Name: u.Name, UGID: u.Name}}, u.Groups...) + u.Groups = append([]types.GroupInfo{{Name: u.Name, GID: &uid, UGID: u.Name}}, u.Groups...) var groupRows []db.GroupRow var localGroups []string @@ -191,7 +191,12 @@ func (m *Manager) UpdateUser(u types.UserInfo) (err error) { // Unexpected error return err } - if errors.Is(err, db.NoDataFoundError{}) { + if !errors.Is(err, db.NoDataFoundError{}) { + // The group already exists in the database, use the existing GID to avoid permission issues. + g.GID = &oldGroup.GID + } + + if g.GID == nil { // The group does not exist in the database, so we generate a unique GID for it. Similar to the RegisterUser // call above, this also registers a temporary group in our NSS handler. We remove that temporary group // before returning from this function, at which point the group is added to the database (so we don't need @@ -202,11 +207,7 @@ func (m *Manager) UpdateUser(u types.UserInfo) (err error) { } defer cleanup() - g.GID = &gid - } else { - // The group already exists in the database, use the existing GID to avoid permission issues. - g.GID = &oldGroup.GID } groupRows = append(groupRows, db.NewGroupRow(g.Name, *g.GID, g.UGID))
internal/users/manager_test.go+4 −5 modified@@ -202,8 +202,7 @@ func TestUpdateUser(t *testing.T) { require.NoError(t, err, "Setup: could not create database from testdata") } - // One GID is generated for the user private group - gids := []uint32{11110} + var gids []uint32 for _, group := range groupsCases[tc.groupsCase] { if group.GID != 0 { gids = append(gids, group.GID) @@ -330,7 +329,6 @@ func TestUpdateBrokerForUser(t *testing.T) { } } -//nolint:dupl // This is not a duplicate test func TestUserByIDAndName(t *testing.T) { tests := map[string]struct { uid uint32 @@ -377,11 +375,13 @@ func TestUserByIDAndName(t *testing.T) { return } - // Registering a temporary user creates it with a random UID and random gecos, so we have to make it + // Registering a temporary user creates it with a random UID, GID, and gecos, so we have to make it // deterministic before comparing it with the golden file if tc.isTempUser { require.Equal(t, tc.uid, user.UID) user.UID = 0 + require.Equal(t, tc.uid, user.GID) + user.GID = 0 require.NotEmpty(t, user.Gecos) user.Gecos = "" } @@ -422,7 +422,6 @@ func TestAllUsers(t *testing.T) { } } -//nolint:dupl // This is not a duplicate test func TestGroupByIDAndName(t *testing.T) { tests := map[string]struct { gid uint32
internal/users/tempentries/preauth.go+17 −3 modified@@ -90,10 +90,11 @@ func (r *preAuthUserRecords) userByLogin(loginName string) (types.UserEntry, err } func preAuthUserEntry(user preAuthUser) types.UserEntry { - // TODO: Should we set the GID to something else than 0 (i.e. the GID of the root primary group)? return types.UserEntry{ - Name: user.name, - UID: user.uid, + Name: user.name, + UID: user.uid, + // The UID is also the GID of the user private group (see https://wiki.debian.org/UserPrivateGroups#UPGs) + GID: user.uid, Gecos: user.loginName, Dir: "/nonexistent", Shell: "/usr/sbin/nologin", @@ -176,6 +177,19 @@ func (r *preAuthUserRecords) isUniqueUID(uid uint32, tmpName string) (bool, erro return false, nil } } + + groupEntries, err := localentries.GetGroupEntries() + if err != nil { + return false, fmt.Errorf("failed to get group entries: %w", err) + } + for _, group := range groupEntries { + if group.GID == uid { + // A group with the same ID already exists, so we can't use that ID as the GID of the temporary user + log.Debugf(context.Background(), "ID %d already in use by group %q", uid, group.Name) + return false, fmt.Errorf("group with GID %d already exists", uid) + } + } + return true, nil }
internal/users/tempentries/testdata/golden/TestPreAuthUserByIDAndName/Successfully_get_a_user_by_ID_and_name+1 −1 modified@@ -1,6 +1,6 @@ name: "" uid: 12345 -gid: 0 +gid: 12345 gecos: test dir: /nonexistent shell: /usr/sbin/nologin
internal/users/tempentries/testdata/golden/TestPreAuthUser/No_error_when_registering_a_pre-auth_user_with_the_same_name+1 −1 modified@@ -1,6 +1,6 @@ name: "" uid: 12345 -gid: 0 +gid: 12345 gecos: test dir: /nonexistent shell: /usr/sbin/nologin
internal/users/tempentries/testdata/golden/TestPreAuthUser/Successfully_register_a_pre-auth_user+1 −1 modified@@ -1,6 +1,6 @@ name: "" uid: 12345 -gid: 0 +gid: 12345 gecos: test dir: /nonexistent shell: /usr/sbin/nologin
internal/users/tempentries/testdata/golden/TestPreAuthUser/Successfully_register_a_pre-auth_user_if_the_first_generated_UID_is_already_in_use+1 −1 modified@@ -1,6 +1,6 @@ name: "" uid: 12345 -gid: 0 +gid: 12345 gecos: test dir: /nonexistent shell: /usr/sbin/nologin
internal/users/tempentries/testdata/golden/TestRegisterUser/Successfully_register_a_new_user+1 −1 modified@@ -1,6 +1,6 @@ name: authd-temp-users-test uid: 12345 -gid: 0 +gid: 12345 gecos: "" dir: /nonexistent shell: /usr/sbin/nologin
internal/users/tempentries/testdata/golden/TestRegisterUser/Successfully_register_a_user_if_the_first_generated_UID_is_already_in_use+1 −1 modified@@ -1,6 +1,6 @@ name: authd-temp-users-test uid: 12345 -gid: 0 +gid: 12345 gecos: "" dir: /nonexistent shell: /usr/sbin/nologin
internal/users/tempentries/testdata/golden/TestRegisterUser/Successfully_register_a_user_if_the_pre-auth_user_already_exists+1 −1 modified@@ -1,6 +1,6 @@ name: authd-temp-users-test uid: 12345 -gid: 0 +gid: 12345 gecos: "" dir: /nonexistent shell: /usr/sbin/nologin
internal/users/tempentries/testdata/golden/TestUserByIDAndName/Successfully_get_a_user_by_ID+1 −1 modified@@ -1,6 +1,6 @@ name: authd-temp-users-test uid: 12345 -gid: 0 +gid: 12345 gecos: "" dir: /nonexistent shell: /usr/sbin/nologin
internal/users/tempentries/testdata/golden/TestUserByIDAndName/Successfully_get_a_user_by_name+1 −1 modified@@ -1,6 +1,6 @@ name: authd-temp-users-test uid: 12345 -gid: 0 +gid: 12345 gecos: "" dir: /nonexistent shell: /usr/sbin/nologin
internal/users/tempentries/users.go+17 −3 modified@@ -62,10 +62,11 @@ func (r *temporaryUserRecords) userByName(name string) (types.UserEntry, error) } func userEntry(user userRecord) types.UserEntry { - // TODO: Should we set the GID to something else than 0 (i.e. the GID of the root primary group)? return types.UserEntry{ - Name: user.name, - UID: user.uid, + Name: user.name, + UID: user.uid, + // The UID is also the GID of the user private group (see https://wiki.debian.org/UserPrivateGroups#UPGs) + GID: user.uid, Gecos: user.gecos, Dir: "/nonexistent", Shell: "/usr/sbin/nologin", @@ -91,6 +92,19 @@ func (r *temporaryUserRecords) uniqueNameAndUID(name string, uid uint32, tmpID s return false, nil } } + + groupEntries, err := localentries.GetGroupEntries() + if err != nil { + return false, fmt.Errorf("failed to get group entries: %w", err) + } + for _, group := range groupEntries { + if group.GID == uid { + // A group with the same ID already exists, so we can't use that ID as the GID of the temporary user. + log.Debugf(context.Background(), "ID %d already in use by group %q", uid, group.Name) + return false, fmt.Errorf("group with GID %d already exists", uid) + } + } + return true, nil }
internal/users/testdata/golden/TestUpdateUser/GID_does_not_change_if_group_with_same_name_and_empty_UGID_exists+3 −3 modified@@ -1,20 +1,20 @@ users: - name: user1 uid: 1111 - gid: 11110 + gid: 1111 gecos: gecos for user1 dir: /home/user1 shell: /bin/bash groups: - name: user1 - gid: 11110 + gid: 1111 ugid: user1 - name: group1 gid: 11111 ugid: "1" users_to_groups: - uid: 1111 - gid: 11110 + gid: 1111 - uid: 1111 gid: 11111 schema_version: 1
internal/users/testdata/golden/TestUpdateUser/GID_does_not_change_if_group_with_same_UGID_exists+3 −3 modified@@ -1,20 +1,20 @@ users: - name: user1 uid: 1111 - gid: 11110 + gid: 1111 gecos: gecos for user1 dir: /home/user1 shell: /bin/bash groups: - name: user1 - gid: 11110 + gid: 1111 ugid: user1 - name: renamed-group gid: 11111 ugid: "12345678" users_to_groups: - uid: 1111 - gid: 11110 + gid: 1111 - uid: 1111 gid: 11111 schema_version: 1
internal/users/testdata/golden/TestUpdateUser/Names_of_authd_groups_are_stored_in_lowercase+3 −3 modified@@ -1,20 +1,20 @@ users: - name: user1 uid: 1111 - gid: 11110 + gid: 1111 gecos: gecos for user1 dir: /home/user1 shell: /bin/bash groups: - name: user1 - gid: 11110 + gid: 1111 ugid: user1 - name: group1 gid: 11111 ugid: "1" users_to_groups: - uid: 1111 - gid: 11110 + gid: 1111 - uid: 1111 gid: 11111 schema_version: 1
internal/users/testdata/golden/TestUpdateUser/Removing_last_user_from_a_group_keeps_the_group_record+3 −3 modified@@ -1,18 +1,18 @@ users: - name: user1 uid: 1111 - gid: 11110 + gid: 1111 gecos: gecos for user1 dir: /home/user1 shell: /bin/bash groups: - name: user1 - gid: 11110 + gid: 1111 ugid: user1 - name: group1 gid: 11111 ugid: "12345678" users_to_groups: - uid: 1111 - gid: 11110 + gid: 1111 schema_version: 1
internal/users/testdata/golden/TestUpdateUser/Successfully_update_user+3 −3 modified@@ -1,20 +1,20 @@ users: - name: user1 uid: 1111 - gid: 11110 + gid: 1111 gecos: gecos for user1 dir: /home/user1 shell: /bin/bash groups: - name: user1 - gid: 11110 + gid: 1111 ugid: user1 - name: group1 gid: 11111 ugid: "1" users_to_groups: - uid: 1111 - gid: 11110 + gid: 1111 - uid: 1111 gid: 11111 schema_version: 1
internal/users/testdata/golden/TestUpdateUser/Successfully_update_user_updating_local_groups+3 −3 modified@@ -1,20 +1,20 @@ users: - name: user1 uid: 1111 - gid: 11110 + gid: 1111 gecos: gecos for user1 dir: /home/user1 shell: /bin/bash groups: - name: user1 - gid: 11110 + gid: 1111 ugid: user1 - name: group1 gid: 11111 ugid: "1" users_to_groups: - uid: 1111 - gid: 11110 + gid: 1111 - uid: 1111 gid: 11111 schema_version: 1
internal/users/testdata/golden/TestUpdateUser/Successfully_update_user_with_different_capitalization+3 −3 modified@@ -1,18 +1,18 @@ users: - name: user1 uid: 1111 - gid: 11110 + gid: 1111 gecos: gecos for User1 dir: /home/user1 shell: /bin/bash groups: - name: user1 - gid: 11110 + gid: 1111 ugid: user1 - name: group1 gid: 11111 ugid: "12345678" users_to_groups: - uid: 1111 - gid: 11110 + gid: 1111 schema_version: 1
internal/users/testdata/golden/TestUpdateUser/UID_does_not_change_if_user_already_exists+3 −3 modified@@ -1,18 +1,18 @@ users: - name: user1 uid: 1111 - gid: 11110 + gid: 1111 gecos: gecos for user1 dir: /home/user1 shell: /bin/bash groups: - name: user1 - gid: 11110 + gid: 1111 ugid: user1 - name: group1 gid: 11111 ugid: "12345678" users_to_groups: - uid: 1111 - gid: 11110 + gid: 1111 schema_version: 1
nss/integration-tests/integration_test.go+4 −2 modified@@ -141,13 +141,15 @@ func TestIntegration(t *testing.T) { if tc.shouldPreCheck && tc.getentDB == "passwd" { // When pre-checking, the `getent passwd` output contains a randomly generated UID. - // To make the test deterministic, we replace the UID with a placeholder. + // To make the test deterministic, we replace the UID and GID with a placeholder. // The output looks something like this: - // user-pre-check:x:1776689191:0:gecos for user-pre-check:/home/user-pre-check:/usr/bin/bash\n + // user-pre-check:x:1776689191:1776689191:gecos for user-pre-check:/home/user-pre-check:/usr/bin/bash\n fields := strings.Split(got, ":") require.Len(t, fields, 7, "Invalid number of fields in the output: %q", got) // The UID is the third field. fields[2] = "{{UID}}" + // The GID is the fourth field. + fields[3] = "{{GID}}" got = strings.Join(fields, ":") }
nss/integration-tests/testdata/golden/TestIntegration/Check_user_with_broker_if_not_found_in_db+1 −1 modified@@ -1 +1 @@ -user-integration-pre-check-simple:x:{{UID}}:0:gecos for user-integration-pre-check-simple:/home/user-integration-pre-check-simple:/bin/sh +user-integration-pre-check-simple:x:{{UID}}:{{GID}}:gecos for user-integration-pre-check-simple:/home/user-integration-pre-check-simple:/bin/sh
nss/integration-tests/testdata/golden/TestIntegration/Check_user_with_broker_if_not_found_in_db_in_upper_case+1 −1 modified@@ -1 +1 @@ -user-integration-pre-check-simple:x:{{UID}}:0:gecos for user-integration-pre-check-simple:/home/user-integration-pre-check-simple:/bin/sh +user-integration-pre-check-simple:x:{{UID}}:{{GID}}:gecos for user-integration-pre-check-simple:/home/user-integration-pre-check-simple:/bin/sh
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.