CVE-2026-61795: Capsule: hostnameRegexHandler.OnUpdate validates stale (old) Tenant regex, allowing invalid AllowedHostnames regex to bypass webhook validation
Summary
A parameter order bug in internal/webhook/tenant/validation/hostnameregex.go causes the hostnameRegexHandler.OnUpdate webhook to validate the old Tenant object's AllowedHostnames.Regex instead of the new one being submitted. This allows an invalid (malformed) regex to bypass admission validation and be persisted to etcd, causing a Denial of Service for all Ingress operations within the affected tenant.
Details
The TypedHandler[T] interface defines OnUpdate as:
go // handlers.go OnUpdate(c client.Client, reader client.Reader, obj T, old T, decoder admission.Decoder, recorder events.EventRecorder) Func // ^^^ NEW ^^^ OLD
The dispatcher in handler.go:93 calls: go hndl.OnUpdate(c, reader, tnt, old, decoder, recorder) // ^^^ NEW ^^^ OLD
However, hostnameRegexHandler.OnUpdate in hostnameregex.go declares its parameters in reversed order:
go // hostnameregex.go (BUGGY) func (h hostnameRegexHandler) OnUpdate( client.Client, client.Reader, old capsulev1beta2.Tenant, // ← receives NEW tenant (mislabeled as old) tnt capsulev1beta2.Tenant, // ← receives OLD tenant (mislabeled as tnt) ... ) handlers.Func { return func(...) admission.Response { if err := h.validate(tnt, req); err != nil { // ← validates OLD, not NEW return err } return nil } }
All 11 other handlers in the same package declare (tnt, old) correctly. hostnameregex.go is the only one with the swap.
As a result, when a Cluster Admin updates Tenant.Spec.IngressOptions.AllowedHostnames.Regex to a malformed value, the webhook compiles the previous valid regex and returns Allow. The malformed regex is then written to etcd.
Subsequently, every Ingress CREATE or UPDATE in that tenant triggers validatehostnames.go:160:
go matched, = regexp.MatchString(allowedRegex, currentHostname)
regexp.MatchString with an invalid pattern returns (false, error). The error is silently ignored, matched is false, and every hostname is rejected — blocking all Ingress operations in the tenant until the Tenant object is manually corrected by an admin.
PoC
//go:build ignore // Standalone reproducer for hostnameregex.go argument swap bug in Capsule // No external deps - shows the bug logic using only stdlib
package main
import ( "fmt" "regexp" )
// Simulating the Tenant spec structure type AllowedHostnames struct { Regex string }
type IngressOptions struct { AllowedHostnames AllowedHostnames }
type TenantSpec struct { IngressOptions IngressOptions }
type Tenant struct { Name string Spec TenantSpec }
// ========================================================= // BUGGY implementation (hostnameregex.go as-is) // OnUpdate(, , old Tenant, tnt Tenant) → validates OLD // ========================================================= func hostnameValidate(tnt Tenant) error { if tnt.Spec.IngressOptions.AllowedHostnames == nil { return nil } if len(tnt.Spec.IngressOptions.AllowedHostnames.Regex) == 0 { return nil } , err := regexp.Compile(tnt.Spec.IngressOptions.AllowedHostnames.Regex) if err != nil { return fmt.Errorf("Deny: unable to compile allowedHostnames allowedRegex") } return nil }
// Dispatcher calls: OnUpdate(c, reader, newTenant, oldTenant, ...) // Interface says: OnUpdate(c, reader, obj[NEW], old[OLD], ...) // // BUGGY handler receives: (old, tnt) meaning: // 3rd param (labeled "old") = actually NEW // 4th param (labeled "tnt") = actually OLD // Then calls h.validate(tnt) = validates the OLD tenant func buggyOnUpdate(newTenant, oldTenant Tenant) error { // BUG: parameters are SWAPPED vs the interface contract old := newTenant // dispatcher's "new" arrives as "old" in this function tnt := oldTenant // dispatcher's "old" arrives as "tnt" in this function = old // unused in the real code too return hostnameValidate(tnt) // validates OLD, not NEW }
// CORRECT implementation (what it should be) func correctOnUpdate(newTenant, oldTenant Tenant) error { = oldTenant return hostnameValidate(newTenant) // validates NEW }
// Simulate ingress hostname validation AFTER bad regex is stored func validateIngressHostname(tenant Tenant, hostname string) bool { if tenant.Spec.IngressOptions.AllowedHostnames == nil { return true } allowedRegex := tenant.Spec.IngressOptions.AllowedHostnames.Regex if len(allowedRegex) == 0 { return true } // This is validatehostnames.go:160 - error is IGNORED matched, := regexp.MatchString(allowedRegex, hostname) return matched }
func main() { fmt.Println("=== Capsule Bug Reproducer: hostnameregex.go argument swap ===") fmt.Println()
oldTenant := &Tenant{ Name: "demo-tenant", Spec: TenantSpec{ IngressOptions: IngressOptions{ AllowedHostnames: &AllowedHostnames{ Regex: ^[\w.-]+\.example\.com$, // valid regex }, }, }, }
// Attacker (cluster admin) sets an INVALID regex in the new spec newTenant := &Tenant{ Name: "demo-tenant", Spec: TenantSpec{ IngressOptions: IngressOptions{ AllowedHostnames: &AllowedHostnames{ Regex: [invalid-regex(, // INVALID regex }, }, }, }
fmt.Printf("Old tenant regex: %q (valid)\n", oldTenant.Spec.IngressOptions.AllowedHostnames.Regex) fmt.Printf("New tenant regex: %q (INVALID)\n", newTenant.Spec.IngressOptions.AllowedHostnames.Regex) fmt.Println()
// Step 1: Webhook runs OnUpdate fmt.Println("--- Step 1: Webhook OnUpdate ---")
err := buggyOnUpdate(newTenant, oldTenant) if err != nil { fmt.Printf("[BUGGY] Webhook DENIES update: %v\n", err) } else { fmt.Println("[BUGGY] Webhook ALLOWS update (validates OLD regex) ← WRONG") }
err = correctOnUpdate(newTenant, oldTenant) if err != nil { fmt.Printf("[CORRECT] Webhook DENIES update: %v ← EXPECTED\n", err) } else { fmt.Println("[CORRECT] Webhook ALLOWS update") }
// Step 2: Invalid regex now stored in etcd - simulate ingress validation fmt.Println() fmt.Println("--- Step 2: Ingress creation after bad regex stored ---") storedTenant := newTenant // bad regex is now in etcd
hostnames := []string{ "app.example.com", "api.example.com", "evil.attacker.com", }
for , h := range hostnames { allowed := validateIngressHostname(storedTenant, h) fmt.Printf(" Ingress hostname %q → allowed=%v", h, allowed) if !allowed { fmt.Print(" ← BLOCKED (DoS: invalid regex causes all hostnames to fail)") } fmt.Println() }
fmt.Println() fmt.Println("=== Result ===") fmt.Println("Invalid regex bypasses webhook validation and gets stored.") fmt.Println("All subsequent Ingress create/update in this tenant are BLOCKED.") fmt.Println("CWE-697: Incorrect Comparison — wrong Tenant object is validated.") }
go // Simulates the buggy webhook behaviour oldTenant := &Tenant{AllowedRegex: ^[\w-]+\.example\.com$} // valid newTenant := &Tenant{AllowedRegex: [invalid-regex(} // malformed
// Buggy OnUpdate: validates oldTenant (valid) → ALLOW // Correct OnUpdate: validates newTenant (invalid) → DENY
// After malformed regex is stored, all ingress hostnames are rejected: matched, := regexp.MatchString([invalid-regex(, "app.example.com") // matched = false, error ignored → Ingress blocked
Fix
Swap the parameter names in hostnameregex.go to match the interface contract:
go // BEFORE (buggy) func (h hostnameRegexHandler) OnUpdate( client.Client, client.Reader, old capsulev1beta2.Tenant, tnt capsulev1beta2.Tenant, ...
// AFTER (fixed) func (h hostnameRegexHandler) OnUpdate( client.Client, client.Reader, tnt capsulev1beta2.Tenant, old capsulev1beta2.Tenant, ...
Impact
A Cluster Admin (or a compromised admin account) can — intentionally or via a typo — set a malformed AllowedHostnames.Regex on any Tenant. The webhook silently accepts the update. All users in the affected tenant are subsequently unable to create or update any Ingress resource until an admin manually corrects the Tenant spec. This constitutes a targeted Denial of Service against the tenant's ingress layer.
Other sources
Capsule is a multi-tenancy and policy-based framework for Kubernetes. From 0.13.0 until 0.13.7, hostnameRegexHandler.OnUpdate in internal/webhook/tenant/validation/hostnameregex.go reverses the new and old Tenant parameters and validates the previous AllowedHostnames.Regex instead of the submitted value. A cluster administrator can therefore store a malformed AllowedHostnames.Regex after the webhook accepts the update based on stale valid state. Subsequent Ingress creation or update reaches validatehostnames.go, which evaluates the malformed pattern, ignores the regular-expression error, and treats every hostname as unmatched, blocking Ingress operations for the affected tenant until an administrator repairs the Tenant configuration. This issue is fixed in version 0.13.7.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
go/github.com/projectcapsule/capsuleto a version that resolves this vulnerability.Fixed in 0.13.7 - Upgrade
Upgrade to a fixed release to a version that resolves this vulnerability.
Fixed in 0.13.7 - Configuration
In hostname_regex.go, fix the OnUpdate signature/usage so the webhook validates the submitted NEW Tenant's AllowedHostnames.Regex (not the OLD Tenant's). The material states this file is the only one with reversed parameter order in versions 0.13.0–0.13.7 and that the fix is to swap the parameter names/order to match the interface contract, preventing malformed regex from being persisted to etcd.
Capsule internal/webhook/tenant/validation/hostname_regex.go OnUpdate parameter order (new vs old Tenant) = Swap parameters so handler validates tnt.Spec.IngressOptions.AllowedHostnames.Regex from the NEW Tenant object per TypedHandler interface contract
Event History
Frequently Asked Questions
Who can exploit this issue in practice?
A cluster administrator who can update Tenant resources can submit a malformed AllowedHostnames.Regex. The published vector requires high privileges and does not require user interaction.
What is the operational impact after a malformed regex is accepted?
Ingress creation and updates for the affected tenant are blocked because hostname validation evaluates the malformed pattern, ignores the regex error, and treats all hostnames as unmatched. Availability is affected; the supplied impact vector indicates no confidentiality or integrity impact.
Are default or unaffected Tenant configurations impacted?
The issue requires an update that stores a malformed AllowedHostnames.Regex after validation uses the prior Tenant state. Tenants without such a malformed stored regex are not described as experiencing the Ingress disruption.
What should be done if immediate upgrading is not possible?
Review Tenant AllowedHostnames.Regex values and repair any malformed expressions, particularly for tenants whose Ingress creates or updates are failing. Restrict Tenant update access to trusted cluster administrators.
How can an administrator identify an affected tenant?
Look for Tenant configurations containing an invalid AllowedHostnames.Regex and correlate them with failed Ingress creation or update operations for that tenant. Repairing the Tenant regex restores hostname matching behavior described by the advisory.