CVE-2022-46167 (Go, published 2022-12-02T19:15Z)
Description: value: Capsule is a multi-tenancy and policy-based framework for Kubernetes. Prior to version 0.1.3, a ServiceAccount deployed in a Tenant Namespace, when granted with `PATCH` capabilities on its own Namespace, is able to edit it and remove the Owner Reference, breaking the reconciliation of the Capsule Operator and removing all the enforcement like Pod Security annotations, Network Policies, Limit Range and Resource Quota items. An attacker could detach the Namespace from a Tenant that is forbidding starting privileged Pods using the Pod Security labels by removing the OwnerReference, removing the enforcement labels, and being able to start privileged containers that would be able to start a generic Kubernetes privilege escalation. Patches have been released for version 0.1.3. No known workarounds are available.

Severity: nan (CVSS v3 base score: 8.8)
Weakness: CWE-863 \u2014 Incorrect Authorization
The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.
Repository: https://github.com/clastix/capsule
Files changed:
controllers/tenant/namespaces.go
main.go
pkg/utils/node_selector.go
pkg/webhook/namespace/freezed.go
pkg/webhook/namespace/owner_reference.go
pkg/webhook/namespace/patch.go
pkg/webhook/namespace/user_metadata.go
pkg/webhook/ownerreference/patching.go
pkg/webhook/tenant/cordoning.go
pkg/webhook/utils/in_capsule_groups.go
pkg/webhook/utils/is_capsule_user.go
Commit message: fix: preventing serviceaccount privilege escalation


Vulnerable code:
				var selector []string
				for k, v := range tnt.Spec.NodeSelector {
					selector = append(selector, fmt.Sprintf("%s=%s", k, v))
				}
				annotations["scheduler.alpha.kubernetes.io/node-selector"] = strings.Join(selector, ",")
		route.OwnerReference(utils.InCapsuleGroups(cfg, ownerreference.Handler(cfg))),
		if tnt.IsCordoned() && utils.IsCapsuleUser(req, r.configuration.UserGroups()) {
		if tnt.IsCordoned() && utils.IsCapsuleUser(req, r.configuration.UserGroups()) {
		return h.setOwnerRef(ctx, req, client, decoder, recorder)
	if tnt.IsCordoned() && utils.IsCapsuleUser(req, h.configuration.UserGroups()) {
		if !IsCapsuleUser(req, h.configuration.UserGroups()) {
		if !IsCapsuleUser(req, h.configuration.UserGroups()) {
		if !IsCapsuleUser(req, h.configuration.UserGroups()) {
func IsCapsuleUser(req admission.Request, userGroups []string) bool {

Fixed code:
	"github.com/clastix/capsule/pkg/utils"
				annotations = utils.BuildNodeSelector(tnt, annotations)
		route.OwnerReference(utils.InCapsuleGroups(cfg, namespacewebhook.OwnerReferenceHandler(), ownerreference.Handler(cfg))),
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0

package utils

import (
	"fmt"
	"sort"
	"strings"

	capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)

const (
	NodeSelectorAnnotation = "scheduler.alpha.kubernetes.io/node-selector"
)

func BuildNodeSelector(tnt *capsulev1beta1.Tenant, nsAnnotations map[string]string) map[string]string {
	if nsAnnotations == nil {
		nsAnnotations = make(map[string]string)
	}

	selector := make([]string, 0, len(tnt.Spec.NodeSelector))

	for k, v := range tnt.Spec.NodeSelector {
		selector = append(selector, fmt.Sprintf("%s=%s", k, v))
	}
	// Sorting the resulting slice: iterating over maps is randomized, and we could end-up
	// in multiple reconciliations upon multiple node selectors.
	sort.Strings(selector)

	nsAnnotations[NodeSelectorAnnotation] = strings.Join(selector, ",")

	return nsAnnotations
}
		if tnt.IsCordoned() && utils.IsCapsuleUser(ctx, req, c, r.configuration.UserGroups()) {
		if tnt.IsCordoned() && utils.IsCapsuleUser(ctx, req, c, r.configuration.UserGroups()) {
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0

package namespace

import (
	"context"
	"fmt"
	"net/http"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/client-go/tools/record"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

	capsulewebhook "github.com/clastix/capsule/pkg/webhook"
	"github.com/clastix/capsule/pkg/webhook/utils"
)

type ownerReferenceHandler struct{}

func OwnerReferenceHandler() capsulewebhook.Handler {
	return &ownerReferenceHandler{}
}

func (r *ownerReferenceHandler) OnCreate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
	return func(ctx context.Context, req admission.Request) *admission.Response {
		return nil
	}
}

func (r *ownerReferenceHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
	return func(ctx context.Context, req admission.Request) *admission.Response {
		return nil
	}
}

func (r *ownerReferenceHandler) OnUpdate(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
	return func(ctx context.Context, req admission.Request) *admission.Response {
		oldNs := &corev1.Namespace{}
		if err := decoder.DecodeRaw(req.OldObject, oldNs); err != nil {
			return utils.ErroredResponse(err)
		}

		newNs := &corev1.Namespace{}
		if err := decoder.Decode(req, newNs); err != nil {
			return utils.ErroredResponse(err)
		}

		if len(newNs.OwnerReferences) == 0 {
			response := admission.Errored(http.StatusBadRequest, fmt.Errorf("the OwnerReference cannot be removed"))

			return &response
		}

		if oldNs.GetOwnerReferences()[0].UID != newNs.GetOwnerReferences()[0].UID {
			response := admission.Errored(http.StatusBadRequest, fmt.Errorf("the OwnerReference cannot be changed"))

			return &response
		}

		return nil
	}
}

		if len(tnt.Spec.NodeSelector) > 0 {
			v, ok := newNs.GetAnnotations()["scheduler.alpha.kubernetes.io/node-selector"]
			if !ok {
				response := admission.Denied("the node-selector annotation is enforced, cannot be removed")

				recorder.Eventf(tnt, corev1.EventTypeWarning, "ForbiddenNodeSelectorDeletion", string(response.Result.Reason))

				return &response
			}

			if v != oldNs.GetAnnotations()["scheduler.alpha.kubernetes.io/node-selector"] {
				response := admission.Denied("the the node-selector annotation is enforced, cannot be updated")

				recorder.Eventf(tnt, corev1.EventTypeWarning, "ForbiddenNodeSelectorUpdate", string(response.Result.Reason))

				return &response
			}
		}

		return nil
	if tnt.IsCordoned() && utils.IsCapsuleUser(ctx, req, clt, h.configuration.UserGroups()) {
		if !IsCapsuleUser(ctx, req, client, h.configuration.UserGroups()) {
		if !IsCapsuleUser(ctx, req, client, h.configuration.UserGroups()) {
		if !IsCapsuleUser(ctx, req, client, h.configuration.UserGroups()) {
	"context"
	"strings"

	"k8s.io/apimachinery/pkg/fields"
	"k8s.io/apimachinery/pkg/util/sets"
	"sigs.k8s.io/controller-runtime/pkg/client"
	capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
func IsCapsuleUser(ctx context.Context, req admission.Request, clt client.Client, userGroups []string) bool {
	// nolint:nestif
	if sets.NewString(req.UserInfo.Groups...).Has("system:serviceaccounts") {
		parts := strings.Split(req.UserInfo.Username, ":")

		targetNamespace := parts[2]

		if len(targetNamespace) > 0 {
			tl := &capsulev1beta1.TenantList{}
			if err := clt.List(ctx, tl, client.MatchingFieldsSelector{Selector: fields.OneTermEqualSelector(".status.namespaces", targetNamespace)}); err != nil {
				return false
			}

			if len(tl.Items) == 1 {
				return true
			}
		}
	}