GHSA-f94q-w3w8-cj67: Medium severity go/github.com/projectcapsule/capsule vulnerability

Published Sep 18, 2026
·
Updated

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.

Affected Software

1 affected componentFixes available
go/github.com/projectcapsule/capsule>=0.13.0<0.13.7
0.13.7

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade go/github.com/projectcapsule/capsule to a version that resolves this vulnerability.

    Fixed in 0.13.7
  2. Configuration

    In hostname_regex.go, change h.OnUpdate’s parameter order to match the TypedHandler[T] interface/dispatcher contract (i.e., validate newTenant.Spec.IngressOptions.AllowedHostnames.Regex, not oldTenant’s). This fixes the bug where the webhook currently validates the old tenant object and ignores the compile error, allowing malformed AllowedHostnames.Regex to be persisted to etcd and causing a DoS for subsequent Ingress CREATE/UPDATE operations in the tenant.

    internal/webhook/tenant/validation/hostname_regex.go OnUpdate parameter order (new vs old Tenant) = Swap the parameters so the handler validates the NEW Tenant object's AllowedHostnames.Regex

Event History

Sep 18, 2026
Advisory Published
via GitHub·05:14 PM
Data Sourced
via GitHub·05:14 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Who can exploit this issue?

An attacker needs high privileges and the ability to submit an update to a Tenant object. The issue is relevant where Tenant AllowedHostnames.Regex values can be changed through the admission webhook.

2

What is the operational impact after a malformed regex is accepted?

The malformed regex can be persisted to etcd and cause a denial of service for all Ingress operations within the affected tenant. Confidentiality and integrity impact are not indicated by the provided severity vector.

3

How can I check whether a tenant is already affected?

Inspect Tenant objects for malformed AllowedHostnames.Regex values that may have been accepted during an update. Affected tenants may experience failures affecting all Ingress operations.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203