{"id":"GHSA-gxjc-74v5-3vx3","summary":"Capsule: Malformed ForbiddenAnnotations.Regex can bypass Tenant validation and trigger namespace admission panic","details":"### Summary\nA validation bug in `internal/webhook/tenant/validation/forbidden_annotations_regex.go` allows an invalid `ForbiddenAnnotations.Regex` value to bypass Tenant admission on update. The webhook compiles `ForbiddenLabels.Regex` for both labels and annotations, so a malformed annotations regex can be persisted. Once stored, namespace admission later evaluates the bad regex through `pkg/api/forbidden_list.go`, where `regexp.MustCompile` can panic and cause admission failure.\n\n### Details\nIn `internal/webhook/tenant/validation/forbidden_annotations_regex.go`, `OnUpdate` validates the new Tenant object, but the loop compiles `tnt.Spec.NamespaceOptions.ForbiddenLabels.Regex` for both `labels` and `annotations`. That means an invalid `ForbiddenAnnotations.Regex` is never validated if `ForbiddenLabels.Regex` is valid.\n\nRelevant paths:\n- `internal/webhook/tenant/validation/forbidden_annotations_regex.go`\n- `internal/webhook/namespace/validation/user_metadata.go`\n- `pkg/api/forbidden_list.go`\n\nNamespace admission later calls `api.ValidateForbidden(...)`, and `ForbiddenListSpec.RegexMatch()` uses `regexp.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.\n\n\n### PoC\n1. Update a Tenant so that:\n   - `spec.namespaceOptions.forbiddenLabels.regex` is valid\n   - `spec.namespaceOptions.forbiddenAnnotations.regex` is malformed, for example: `[invalid-regex(`\n2. The Tenant update is accepted because the webhook compiles the labels regex for both fields.\n3. Create or update a Namespace that triggers forbidden metadata validation.\n4. The namespace admission path reaches `regexp.MustCompile(...)` and panics.\n\n```\npackage main\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n)\n\ntype ForbiddenListSpec struct {\n\tRegex string\n}\n\ntype NamespaceOptions struct {\n\tForbiddenLabels      ForbiddenListSpec\n\tForbiddenAnnotations ForbiddenListSpec\n}\n\ntype Tenant struct {\n\tNamespaceOptions *NamespaceOptions\n}\n\nfunc validateTenantUpdate(tnt *Tenant) error {\n\tif tnt.NamespaceOptions == nil {\n\t\treturn nil\n\t}\n\n\tannotationsToCheck := map[string]string{\n\t\t\"labels\":      tnt.NamespaceOptions.ForbiddenLabels.Regex,\n\t\t\"annotations\": tnt.NamespaceOptions.ForbiddenAnnotations.Regex,\n\t}\n\n\tfor scope, annotation := range annotationsToCheck {\n\t\tif _, err := regexp.Compile(tnt.NamespaceOptions.ForbiddenLabels.Regex); err != nil {\n\t\t\treturn fmt.Errorf(\"deny update: unable to compile %s regex for forbidden %s\", annotation, scope)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc validateForbidden(metadata map[string]string, forbidden ForbiddenListSpec) error {\n\tfor key := range metadata {\n\t\tif forbidden.Regex != \"\" {\n\t\t\tif regexp.MustCompile(forbidden.Regex).MatchString(key) {\n\t\t\t\treturn fmt.Errorf(\"forbidden key matched: %s\", key)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc main() {\n\toldTenant := &Tenant{\n\t\tNamespaceOptions: &NamespaceOptions{\n\t\t\tForbiddenLabels:      ForbiddenListSpec{Regex: `^[a-z0-9-]+$`},\n\t\t\tForbiddenAnnotations: ForbiddenListSpec{Regex: `^[a-z0-9-]+$`},\n\t\t},\n\t}\n\n\tnewTenant := &Tenant{\n\t\tNamespaceOptions: &NamespaceOptions{\n\t\t\tForbiddenLabels:      ForbiddenListSpec{Regex: `^[a-z0-9-]+$`},\n\t\t\tForbiddenAnnotations: ForbiddenListSpec{Regex: `[invalid-regex(`},\n\t\t},\n\t}\n\n\tfmt.Println(\"=== Update step ===\")\n\tif err := validateTenantUpdate(newTenant); err != nil {\n\t\tfmt.Printf(\"unexpected deny: %v\\n\", err)\n\t} else {\n\t\tfmt.Println(\"allowed: malformed ForbiddenAnnotations.Regex bypassed validation\")\n\t}\n\n\tfmt.Println()\n\tfmt.Println(\"=== Namespace step ===\")\n\t_ = oldTenant\n\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tfmt.Printf(\"panic reproduced from ValidateForbidden: %v\\n\", r)\n\t\t}\n\t}()\n\n\t_ = validateForbidden(map[string]string{\"example\": \"value\"}, ForbiddenListSpec{Regex: `[invalid-regex(`})\n\tfmt.Println(\"no panic, unexpected\")\n}\n```\nExpected output:\n\n```text\n=== Update step ===\nallowed: malformed ForbiddenAnnotations.Regex bypassed validation\n\n=== Namespace step ===\npanic reproduced from ValidateForbidden: regexp: Compile(`[invalid-regex(`): error parsing regexp: missing closing ]: `[invalid-regex(`\n```\n\n\n\n### Impact\nAn attacker who can update the Tenant configuration can persist a malformed `ForbiddenAnnotations.Regex` and cause namespace admission failures for the affected tenant. This can result in a tenant-scoped denial of service.","aliases":["CVE-2026-61794","GO-2026-6521"],"modified":"2026-09-28T17:11:19.835452014Z","published":"2026-09-18T17:14:26Z","database_specific":{"cwe_ids":["CWE-20"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-09-18T17:14:26Z","nvd_published_at":null},"references":[{"type":"WEB","url":"https://github.com/projectcapsule/capsule/security/advisories/GHSA-gxjc-74v5-3vx3"},{"type":"WEB","url":"https://github.com/projectcapsule/capsule/pull/1983"},{"type":"WEB","url":"https://github.com/projectcapsule/capsule/commit/8d89d6865df6f41c7faa22fc9e807a57b01bfd0e"},{"type":"PACKAGE","url":"https://github.com/projectcapsule/capsule"},{"type":"WEB","url":"https://github.com/projectcapsule/capsule/releases/tag/v0.13.7"}],"affected":[{"package":{"name":"github.com/projectcapsule/capsule","ecosystem":"Go","purl":"pkg:golang/github.com/projectcapsule/capsule"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0.13.0"},{"fixed":"0.13.7"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-gxjc-74v5-3vx3/GHSA-gxjc-74v5-3vx3.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H"}]}