VYPR
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.

PackageAffected versionsPatched versions
github.com/ubuntu/authdGo
< 0.5.40.5.4

Affected products

1

Patches

1
619ce8e55953

Fix new users logging in via SSH being part of the root group

https://github.com/ubuntu/authdAdrian DombeckMay 27, 2025via ghsa
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

4

News mentions

0

No linked articles in our index yet.