VYPR
High severityNVD Advisory· Published Aug 20, 2024· Updated Aug 14, 2025

Capsule tenant owner with "patch namespace" permission can hijack system namespaces

CVE-2024-39690

Description

Capsule is a multi-tenancy and policy-based framework for Kubernetes. In Capsule v0.7.0 and earlier, the tenant-owner can patch any arbitrary namespace that has not been taken over by a tenant (i.e., namespaces without the ownerReference field), thereby gaining control of that namespace. Version 0.7.1 contains a patch.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/projectcapsule/capsuleGo
< 0.7.10.7.1

Affected products

1

Patches

1
d620b0457dde

Merge commit from fork

https://github.com/projectcapsule/capsuleOliver BählerAug 20, 2024via ghsa
3 files changed · +152 5
  • charts/capsule/templates/configuration-default.yaml+2 1 modified
    @@ -25,4 +25,5 @@ spec:
       nodeMetadata:
         {{- toYaml . | nindent 4 }}
       {{- end }}
    -{{- end }}
    \ No newline at end of file
    +{{- end }}
    +
    
  • e2e/namespace_hijacking_test.go+119 0 added
    @@ -0,0 +1,119 @@
    +//go:build e2e
    +
    +// Copyright 2020-2023 Project Capsule Authors.
    +// SPDX-License-Identifier: Apache-2.0
    +
    +package e2e
    +
    +import (
    +	"context"
    +	"fmt"
    +	corev1 "k8s.io/api/core/v1"
    +	"math/rand"
    +
    +	. "github.com/onsi/ginkgo/v2"
    +	. "github.com/onsi/gomega"
    +	capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
    +	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    +	"k8s.io/apimachinery/pkg/types"
    +)
    +
    +var _ = Describe("creating several Namespaces for a Tenant", func() {
    +	tnt := &capsulev1beta2.Tenant{
    +		ObjectMeta: metav1.ObjectMeta{
    +			Name: "capsule-ns-attack-1",
    +		},
    +		Spec: capsulev1beta2.TenantSpec{
    +			Owners: capsulev1beta2.OwnerListSpec{
    +				{
    +					Name: "charlie",
    +					Kind: "User",
    +				},
    +				{
    +					Kind: "ServiceAccount",
    +					Name: "system:serviceaccount:attacker-system:attacker",
    +				},
    +			},
    +		},
    +	}
    +
    +	kubeSystem := &corev1.Namespace{
    +		ObjectMeta: metav1.ObjectMeta{
    +			Name: "kube-system",
    +		},
    +	}
    +	JustBeforeEach(func() {
    +		EventuallyCreation(func() (err error) {
    +			tnt.ResourceVersion = ""
    +			err = k8sClient.Create(context.TODO(), tnt)
    +
    +			return
    +		}).Should(Succeed())
    +	})
    +	JustAfterEach(func() {
    +		Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
    +
    +	})
    +
    +	It("Can't hijack offlimits namespace", func() {
    +		tenant := &capsulev1beta2.Tenant{}
    +		Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.Name}, tenant)).Should(Succeed())
    +
    +		// Get the namespace
    +		Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: kubeSystem.GetName()}, kubeSystem)).Should(Succeed())
    +
    +		for _, owner := range tnt.Spec.Owners {
    +			cs := ownerClient(owner)
    +
    +			patch := []byte(fmt.Sprintf(`{"metadata":{"ownerReferences":[{"apiVersion":"%s/%s","kind":"Tenant","name":"%s","uid":"%s"}]}}`, capsulev1beta2.GroupVersion.Group, capsulev1beta2.GroupVersion.Version, tenant.GetName(), tenant.GetUID()))
    +
    +			_, err := cs.CoreV1().Namespaces().Patch(context.TODO(), kubeSystem.Name, types.StrategicMergePatchType, patch, metav1.PatchOptions{})
    +			Expect(err).To(HaveOccurred())
    +
    +		}
    +	})
    +
    +	It("Owners can create and attempt to patch new namespaces but patches should not be applied", func() {
    +		for _, owner := range tnt.Spec.Owners {
    +			cs := ownerClient(owner)
    +
    +			// Each owner creates a new namespace
    +			ns := NewNamespace("")
    +			NamespaceCreation(ns, owner, defaultTimeoutInterval).Should(Succeed())
    +
    +			// Attempt to patch the owner references of the new namespace
    +			tenant := &capsulev1beta2.Tenant{}
    +			Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.Name}, tenant)).Should(Succeed())
    +
    +			randomUID := types.UID(fmt.Sprintf("%d", rand.Int()))
    +			randomName := fmt.Sprintf("random-tenant-%d", rand.Int())
    +			patch := []byte(fmt.Sprintf(`{"metadata":{"ownerReferences":[{"apiVersion":"%s/%s","kind":"Tenant","name":"%s","uid":"%s"}]}}`, capsulev1beta2.GroupVersion.Group, capsulev1beta2.GroupVersion.Version, randomName, randomUID))
    +
    +			_, err := cs.CoreV1().Namespaces().Patch(context.TODO(), ns.Name, types.StrategicMergePatchType, patch, metav1.PatchOptions{})
    +			Expect(err).ToNot(HaveOccurred())
    +
    +			retrievedNs := &corev1.Namespace{}
    +			Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.Name}, retrievedNs)).Should(Succeed())
    +
    +			// Check if the namespace has an owner reference with the specific UID and name
    +			hasSpecificOwnerRef := false
    +			for _, ownerRef := range retrievedNs.OwnerReferences {
    +				if ownerRef.UID == randomUID && ownerRef.Name == randomName {
    +					hasSpecificOwnerRef = true
    +					break
    +				}
    +			}
    +			Expect(hasSpecificOwnerRef).To(BeFalse(), "Namespace should not have owner reference with UID %s and name %s", randomUID, randomName)
    +
    +			hasOriginReference := false
    +			for _, ownerRef := range retrievedNs.OwnerReferences {
    +				if ownerRef.UID == tenant.GetUID() && ownerRef.Name == tenant.GetName() {
    +					hasOriginReference = true
    +					break
    +				}
    +			}
    +			Expect(hasOriginReference).To(BeTrue(), "Namespace should have origin reference", tenant.GetUID(), tenant.GetName())
    +		}
    +	})
    +
    +})
    
  • pkg/webhook/ownerreference/patching.go+31 4 modified
    @@ -7,6 +7,7 @@ import (
     	"context"
     	"encoding/json"
     	"fmt"
    +	"k8s.io/apimachinery/pkg/fields"
     	"net/http"
     	"sort"
     	"strings"
    @@ -49,15 +50,26 @@ func (h *handler) OnDelete(client.Client, admission.Decoder, record.EventRecorde
     	}
     }
     
    -func (h *handler) OnUpdate(_ client.Client, decoder admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
    -	return func(_ context.Context, req admission.Request) *admission.Response {
    +func (h *handler) OnUpdate(c client.Client, decoder admission.Decoder, recorder 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)
     		}
     
    -		if len(oldNs.OwnerReferences) == 0 {
    -			return nil
    +		tntList := &capsulev1beta2.TenantList{}
    +		if err := c.List(ctx, tntList, client.MatchingFieldsSelector{
    +			Selector: fields.OneTermEqualSelector(".status.namespaces", oldNs.Name),
    +		}); err != nil {
    +			return utils.ErroredResponse(err)
    +		}
    +
    +		if !h.namespaceIsOwned(oldNs, tntList, req) {
    +			recorder.Eventf(oldNs, corev1.EventTypeWarning, "OfflimitNamespace", "Namespace %s can not be patched", oldNs.GetName())
    +
    +			response := admission.Denied("Denied patch request for this namespace")
    +
    +			return &response
     		}
     
     		newNs := &corev1.Namespace{}
    @@ -101,6 +113,21 @@ func (h *handler) OnUpdate(_ client.Client, decoder admission.Decoder, _ record.
     	}
     }
     
    +func (h *handler) namespaceIsOwned(ns *corev1.Namespace, tenantList *capsulev1beta2.TenantList, req admission.Request) bool {
    +	for _, tenant := range tenantList.Items {
    +		for _, ownerRef := range ns.OwnerReferences {
    +			if !capsuleutils.IsTenantOwnerReference(ownerRef) {
    +				continue
    +			}
    +			if ownerRef.UID == tenant.UID && utils.IsTenantOwner(tenant.Spec.Owners, req.UserInfo) {
    +				return true
    +			}
    +		}
    +	}
    +
    +	return false
    +}
    +
     func (h *handler) setOwnerRef(ctx context.Context, req admission.Request, client client.Client, decoder admission.Decoder, recorder record.EventRecorder) *admission.Response {
     	ns := &corev1.Namespace{}
     	if err := decoder.Decode(req, ns); err != nil {
    

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.