package main
import (
"fmt"
"regexp"
)
type ForbiddenListSpec struct {
Regex string
}
type NamespaceOptions struct {
ForbiddenLabels ForbiddenListSpec
ForbiddenAnnotations ForbiddenListSpec
}
type Tenant struct {
NamespaceOptions *NamespaceOptions
}
func validateTenantUpdate(tnt *Tenant) error {
if tnt.NamespaceOptions == nil {
return nil
}
annotationsToCheck := map[string]string{
"labels": tnt.NamespaceOptions.ForbiddenLabels.Regex,
"annotations": tnt.NamespaceOptions.ForbiddenAnnotations.Regex,
}
for scope, annotation := range annotationsToCheck {
if _, err := regexp.Compile(tnt.NamespaceOptions.ForbiddenLabels.Regex); err != nil {
return fmt.Errorf("deny update: unable to compile %s regex for forbidden %s", annotation, scope)
}
}
return nil
}
func validateForbidden(metadata map[string]string, forbidden ForbiddenListSpec) error {
for key := range metadata {
if forbidden.Regex != "" {
if regexp.MustCompile(forbidden.Regex).MatchString(key) {
return fmt.Errorf("forbidden key matched: %s", key)
}
}
}
return nil
}
func main() {
oldTenant := &Tenant{
NamespaceOptions: &NamespaceOptions{
ForbiddenLabels: ForbiddenListSpec{Regex: `^[a-z0-9-]+$`},
ForbiddenAnnotations: ForbiddenListSpec{Regex: `^[a-z0-9-]+$`},
},
}
newTenant := &Tenant{
NamespaceOptions: &NamespaceOptions{
ForbiddenLabels: ForbiddenListSpec{Regex: `^[a-z0-9-]+$`},
ForbiddenAnnotations: ForbiddenListSpec{Regex: `[invalid-regex(`},
},
}
fmt.Println("=== Update step ===")
if err := validateTenantUpdate(newTenant); err != nil {
fmt.Printf("unexpected deny: %v\n", err)
} else {
fmt.Println("allowed: malformed ForbiddenAnnotations.Regex bypassed validation")
}
fmt.Println()
fmt.Println("=== Namespace step ===")
_ = oldTenant
defer func() {
if r := recover(); r != nil {
fmt.Printf("panic reproduced from ValidateForbidden: %v\n", r)
}
}()
_ = validateForbidden(map[string]string{"example": "value"}, ForbiddenListSpec{Regex: `[invalid-regex(`})
fmt.Println("no panic, unexpected")
}
=== Update step ===
allowed: malformed ForbiddenAnnotations.Regex bypassed validation
=== Namespace step ===
panic reproduced from ValidateForbidden: regexp: Compile(`[invalid-regex(`): error parsing regexp: missing closing ]: `[invalid-regex(`
Summary
A validation bug in
internal/webhook/tenant/validation/forbidden_annotations_regex.goallows an invalidForbiddenAnnotations.Regexvalue to bypass Tenant admission on update. The webhook compilesForbiddenLabels.Regexfor both labels and annotations, so a malformed annotations regex can be persisted. Once stored, namespace admission later evaluates the bad regex throughpkg/api/forbidden_list.go, whereregexp.MustCompilecan panic and cause admission failure.Details
In
internal/webhook/tenant/validation/forbidden_annotations_regex.go,OnUpdatevalidates the new Tenant object, but the loop compilestnt.Spec.NamespaceOptions.ForbiddenLabels.Regexfor bothlabelsandannotations. That means an invalidForbiddenAnnotations.Regexis never validated ifForbiddenLabels.Regexis valid.Relevant paths:
internal/webhook/tenant/validation/forbidden_annotations_regex.gointernal/webhook/namespace/validation/user_metadata.gopkg/api/forbidden_list.goNamespace admission later calls
api.ValidateForbidden(...), andForbiddenListSpec.RegexMatch()usesregexp.MustCompile(in.Regex). If the malformed regex is present in the Tenant spec, any namespace request that reaches this check can panic or fail hard, causing denial of service for namespace operations in the affected tenant.PoC
spec.namespaceOptions.forbiddenLabels.regexis validspec.namespaceOptions.forbiddenAnnotations.regexis malformed, for example:[invalid-regex(regexp.MustCompile(...)and panics.Expected output:
Impact
An attacker who can update the Tenant configuration can persist a malformed
ForbiddenAnnotations.Regexand cause namespace admission failures for the affected tenant. This can result in a tenant-scoped denial of service.