GitHub Advisoryadvisory · Go

GHSA-gjw4-3v3v-rqxg Capsule: Tenant owner bypasses Capsule's forbidden namespace/service/node label and annotation enforcement

HIGH Published 2026-09-18 Modified 2026-09-18 Official page ↗
Aliases
CVE-2026-61672
Ecosystem
Go
Packages
github.com/projectcapsule/capsule
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:H/A:N
Weaknesses
CWE-697, CWE-863
Reviewed
2026-09-18

Affected packages

• Go github.com/projectcapsule/capsule: < 0.13.7 → fixed in 0.13.7

Description

Summary Capsule lets a cluster administrator forbid specific metadata keys that tenant owners must not place on their own resources: `Tenant.spec.namespaceOptions.forbiddenLabels` / `forbiddenAnnotations` (namespaces), `Tenant.spec.serviceOptions.forbiddenLabels` / `forbiddenAnnotations` (Services), and the cluster-wide forbidden worker-node labels/annotations. These lists are an isolation control — they exist to stop a tenant owner from setting metadata that other controllers or admission plugins key on (Pod Security Admission labels, `kubernetes.io/metadata.name`, LoadBalancer/externalIP service annotations, scheduler annotations, vendor labels that grant network reach, etc.). The validating webhooks enforce them through `api.ValidateForbidden`, which calls `ForbiddenListSpec.ExactMatch(key)` for every key the tenant submits. `ExactMatch` is broken. It sorts the denied list case-insensitively (`sort.SliceStable` with a `strings.ToLower` comparator) and then performs a byte-order binary search (`sort.SearchStrings`) over the result. `sort.SearchStrings` is only correct on a slice sorted in plain byte-ascending order. Whenever the denied list contains an entry whose case-insensitive position differs from its byte position — which happens any time the list mixes a capitalised key with lowercase keys, because ASCII uppercase letters (0x41–0x5A) sort *before* lowercase (0x61–0x7A) by byte but are interleaved by `ToLower` — the binary search lands on the wrong index and `ExactMatch` returns false for a key that is literally present in the denied list. The webhook then *allows* the forbidden metadata. A tenant owner (who legitimately holds patch/create rights on their own tenant-owned namespaces and Services) can therefore set a metadata key the administrator explicitly forbade, defeating the control and reaching metadata-driven cross-tenant / system effects of exactly the kind Capsule's forbidden lists are meant to prevent. The bug is deterministic, requires no race, and is present unchanged on `main` HEAD. Affected code (v0.13.5) `pkg/api/forbidden_list.go` — the comparison primitive: ```go func (in ForbiddenListSpec) ExactMatch(value string) (ok bool) { if len(in.Exact) > 0 { sort.SliceStable(in.Exact, func(i, j int) bool { return strings.ToLower(in.Exact[i]) = value` under raw byte comparison. If the slice is not byte-sorted, that index is wrong and the subsequent `in.Exact[i] == value` equality check fails even though `value` is in the slice — a false "not forbidden". `pkg/api/forbidden_list.go` — the public entry point the webhooks call: ```go func ValidateForbidden(metadata map[string]string, forbiddenList ForbiddenListSpec) error { if reflect.DeepEqual(ForbiddenListSpec{}, forbiddenList) { return nil } for key := range metadata { var forbidden, matched bool forbidden = forbiddenList.ExactMatch(key) // <-- buggy matched = forbiddenList.RegexMatch(key) if forbidden || matched { return NewForbiddenError(key, forbiddenList) } } return nil } ``` Reached from (all in `internal/webhook/`): • `namespace/validation/user_metadata.go` → `validateUserMetadata` → `api.ValidateForbidden(labels, options.ForbiddenLabels)` and `api.ValidateForbidden(annotations, options.ForbiddenAnnotations)`. • `service/validating.go` → `api.ValidateForbidden(svc.Labels, tnt.Spec.ServiceOptions.ForbiddenLabels)` and `...ForbiddenAnnotations`. • `node/user_metadata.go` → `getForbiddenNodeLabels` / `getForbiddenNodeAnnotations` call `forbiddenLabels.ExactMatch(...)` directly. (The sibling allow-list primitive `AllowedListSpec.ExactMatch` in `pkg/api/allowed_list.go` has the identical defect, but there the polarity is fail-closed — a missed match wrongly *denies* an allowed class — so it is a correctness annoyance, not a security bypass. The forbidden-list polarity is the one that fails open.) Attacker model / precondition The attacker is a tenant owner — an authenticated, non-cluster-admin principal who already holds Capsule's delegated rights to create/patch their own tenant-owned namespaces and the Services within them (the normal Capsule tenancy model). No additional Kubernetes privilege is required. The single deployment precondition that bounds severity: the administrator's denied list must contain at least one entry whose case-insensitive sort order diverges from its byte order — in practice, the list mixes at least one capitalised key with lowercase keys (or contains non-ASCII keys). A list that is uniformly lowercase (the most common shape) sorts identically under both orders and is *not* affected; an empty list (the chart default) is not affected. Mixed-case denied lists are entirely realistic, however: administrators routinely deny vendor/product-capitalised keys (e.g. `OwnerReference`, `NetworkPolicy`, CamelCase operator labels) alongside lowercase `kubernetes.io/...` keys. Once a single CamelCase entry is present, the broken binary search can also drop *lowercase* entries that share no resemblance to it — in the PoC below, adding a `NetworkPolicy` entry causes the unrelated lowercase `kubernetes.io/metadata.name` entry to escape as well. Any such list silently develops one or more exploitable gaps, and the defender cannot tell from the configuration that enforcement is partially disabled — the webhook reports success. Once the precondition holds, exploitation is deterministic and needs only a single `kubectl label`/`kubectl annotate` (or create) on a resource the tenant already controls. Impact The administrator's forbidden-metadata isolation control is partially and silently bypassable. Concrete consequences depend on which key the gap exposes, but all of them are precisely what the control was configured to stop: • Namespace labels/annotations: a tenant owner sets a label the admin forbade onto a tenant namespace — e.g. a Pod Security Admission `pod-security.kubernetes.io/enforce` override, a `kubernetes.io/metadata.name`-class identity label, or a label that a cluster NetworkPolicy / external controller selects on — re-introducing the multi-tenant-isolation break that Capsule's forbidden-label feature exists to prevent (the same class as the previously-fixed namespace-label-injection isolation issue). • Service labels/annotations: a tenant owner sets a forbidden Service annotation — e.g. a cloud LoadBalancer / `externalIPs` / internal-LB provider annotation the admin denied — influencing network exposure outside the tenant boundary. • Node labels/annotations: for tenants granted node-patch rights, a forbidden node label that the admin meant to protect can be modified, affecting scheduling/topology decisions cluster-wide. Scope is Changed (the webhook protects resources and effects beyond the tenant's own boundary), confidentiality/integrity impact is real but gated by the mixed-case precondition and by which specific key the gap exposes — hence Medium, not High. Proof of Concept (complete — runs on 127.0.0.1 only) This PoC drives the *real* Capsule decision code (`pkg/api`) — the exact function the namespace/service/node webhooks call — with no network and no cluster. It demonstrates the bypass with a realistic mixed-case denied list and includes positive and negative controls so the result is unambiguous. Step 1 — fetch the exact source under test (offline thereafter): ```bash git clone --depth 1 --branch v0.13.5 https://github.com/projectcapsule/capsule.git cd capsule git rev-parse HEAD # expect 34262c5536604762090144b6f8aed3ef2780c18c ``` Step 2 — drop this test into the package under test, `pkg/api/forbidden_bypass_poc_test.go`. The denied list is a realistic three-key administrator policy: …

Related

Alias · CVE (1)

  • CVE-2026-61672 Capsule is a multi-tenancy and policy-based framework for Kubernetes. vulnerability

Weakness · CWE (2)

  • CWE-697 Incorrect Comparison weakness
  • CWE-863 Incorrect Authorization weakness

References