CVE-2026-50569
Description
Fission versions prior to 1.25.0 allowed HTTPTriggers to bypass URL validation via kubectl or direct API calls, enabling path traversal and route collisions.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Fission versions prior to 1.25.0 allowed HTTPTriggers to bypass URL validation via kubectl or direct API calls, enabling path traversal and route collisions.
Vulnerability
Fission versions prior to 1.25.0 contained a vulnerability where the HTTPTriggerSpec.Validate() function did not validate the RelativeURL and Prefix fields. These fields were only validated at the CLI level. Following a change to use API-server CEL for validation, and since CEL also lacked rules for these fields, HTTPTriggers created via kubectl apply or direct Kubernetes REST API calls bypassed all URL-level checks [2].
Exploitation
An authenticated Kubernetes user with HTTPTrigger create permissions could create a malicious HTTPTrigger. This could involve setting an empty RelativeURL or Prefix, using a RelativeURL that does not start with /, setting RelativeURL to /, including .. traversal segments, or colliding with router-owned routes like /router-healthz or the internal /fission-function// prefix [2].
Impact
Successful exploitation allows an attacker to create triggers with unexpected or malicious URLs. This could lead to path traversal, allowing access to unintended resources, or route collisions, potentially disrupting legitimate services or gaining unauthorized access to internal Fission routes [2].
Mitigation
This vulnerability was fixed in Fission version 1.25.0, released on 2024-04-09 [1]. The fix enforces path-safety invariants at both the API server's CEL admission gate and within the HTTPTriggerSpec.Validate() function to ensure consistency between the CLI and the API server [3].
AI Insight generated on Jun 10, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
10deed6bf3f26fix(httptrigger): enforce path-safety at admission (GHSA-vchh) (#3464)
4 files changed · +164 −0
crds/v1/fission.io_httptriggers.yaml+20 −0 modified@@ -237,6 +237,26 @@ spec: required: - functionref type: object + x-kubernetes-validations: + - message: 'HTTPTriggerSpec: at least one of relativeurl or prefix must + be set' + rule: self.relativeurl != '' || (has(self.prefix) && self.prefix != + '') + - message: HTTPTriggerSpec.relativeurl must start with '/', not be '/', + not contain '..' path segments, not collide with a router-owned path + (/router-healthz, /readyz, /_version, /auth/login), and not start + with /fission-function/ + rule: self.relativeurl == '' || (self.relativeurl.startsWith('/') && + self.relativeurl != '/' && !self.relativeurl.matches('(^|/)[.][.](/|$)') + && !(self.relativeurl in ['/router-healthz','/readyz','/_version','/auth/login']) + && !self.relativeurl.startsWith('/fission-function/')) + - message: HTTPTriggerSpec.prefix must start with '/', not be '/', not + contain '..' path segments, not collide with a router-owned path (/router-healthz, + /readyz, /_version, /auth/login), and not start with /fission-function/ + rule: '!has(self.prefix) || self.prefix == '''' || (self.prefix.startsWith(''/'') + && self.prefix != ''/'' && !self.prefix.matches(''(^|/)[.][.](/|$)'') + && !(self.prefix in [''/router-healthz'',''/readyz'',''/_version'',''/auth/login'']) + && !self.prefix.startsWith(''/fission-function/''))' status: description: HTTPTriggerStatus describes the observed state of an HTTPTrigger. properties:
pkg/apis/core/v1/types.go+3 −0 modified@@ -708,6 +708,9 @@ type ( // // HTTPTriggerSpec is for router to expose user functions at the given URL path. + // +kubebuilder:validation:XValidation:rule="self.relativeurl != '' || (has(self.prefix) && self.prefix != '')",message="HTTPTriggerSpec: at least one of relativeurl or prefix must be set" + // +kubebuilder:validation:XValidation:rule="self.relativeurl == '' || (self.relativeurl.startsWith('/') && self.relativeurl != '/' && !self.relativeurl.matches('(^|/)[.][.](/|$)') && !(self.relativeurl in ['/router-healthz','/readyz','/_version','/auth/login']) && !self.relativeurl.startsWith('/fission-function/'))",message="HTTPTriggerSpec.relativeurl must start with '/', not be '/', not contain '..' path segments, not collide with a router-owned path (/router-healthz, /readyz, /_version, /auth/login), and not start with /fission-function/" + // +kubebuilder:validation:XValidation:rule="!has(self.prefix) || self.prefix == '' || (self.prefix.startsWith('/') && self.prefix != '/' && !self.prefix.matches('(^|/)[.][.](/|$)') && !(self.prefix in ['/router-healthz','/readyz','/_version','/auth/login']) && !self.prefix.startsWith('/fission-function/'))",message="HTTPTriggerSpec.prefix must start with '/', not be '/', not contain '..' path segments, not collide with a router-owned path (/router-healthz, /readyz, /_version, /auth/login), and not start with /fission-function/" HTTPTriggerSpec struct { // TODO: remove this field since we have IngressConfig already // Deprecated: the original idea of this field is not for setting Ingress.
pkg/apis/core/v1/validation.go+64 −0 modified@@ -459,9 +459,73 @@ func (spec HTTPTriggerSpec) Validate() error { if spec.CorsConfig != nil { errs = errors.Join(errs, spec.CorsConfig.Validate()) } + + // Path validation. HTTPTrigger has no admission webhook on current main + // (the API server's CEL evaluation is the admission gate); these checks + // mirror the CEL rules on HTTPTriggerSpec so the CLI and the router + // reconciler's status-Condition path agree with what the API server + // admits. Closes GHSA-vchh-r53j-8mpw. + prefix := "" + if spec.Prefix != nil { + prefix = *spec.Prefix + } + if spec.RelativeURL == "" && prefix == "" { + errs = errors.Join(errs, MakeValidationErr(ErrorInvalidValue, "HTTPTriggerSpec", "", + "at least one of relativeurl or prefix must be set")) + } + if spec.RelativeURL != "" { + errs = errors.Join(errs, validateTriggerPath("HTTPTriggerSpec.RelativeURL", spec.RelativeURL)) + } + if prefix != "" { + errs = errors.Join(errs, validateTriggerPath("HTTPTriggerSpec.Prefix", prefix)) + } return errs } +// routerReservedExactPaths are URL paths the router serves itself: liveness +// (/router-healthz), readiness (/readyz), version (/_version), and the +// chart-default auth login (/auth/login). Installations that change the auth +// path must still ensure their custom path does not collide with another +// HTTPTrigger; this list covers the defaults the router ships with. +var routerReservedExactPaths = map[string]struct{}{ + "/router-healthz": {}, + "/readyz": {}, + "/_version": {}, + "/auth/login": {}, +} + +// routerInternalFunctionPrefix is the URL prefix the router serves on its +// internal listener (post-GHSA-3g33-6vg6-27m8) for direct function invocation. +// Triggers under this prefix would shadow internal routes if the public/ +// internal listener split is misconfigured. +const routerInternalFunctionPrefix = "/fission-function/" + +// validateTriggerPath enforces the URL-path safety invariants for RelativeURL +// and Prefix in HTTPTriggerSpec. Keep these checks aligned with the CEL rules +// on HTTPTriggerSpec in types.go. +func validateTriggerPath(field, path string) error { + if !strings.HasPrefix(path, "/") { + return MakeValidationErr(ErrorInvalidValue, field, path, "must start with '/'") + } + if path == "/" { + return MakeValidationErr(ErrorInvalidValue, field, path, "root-only path '/' is not allowed") + } + // Reject any ".." path segment. Splitting on '/' (rather than substring + // match) permits literal names like "..foo" or "foo..bar" while catching + // the traversal form ".." that the router would otherwise resolve away. + if slices.Contains(strings.Split(path, "/"), "..") { + return MakeValidationErr(ErrorInvalidValue, field, path, "must not contain '..' path segments") + } + if _, reserved := routerReservedExactPaths[path]; reserved { + return MakeValidationErr(ErrorInvalidValue, field, path, "collides with a router-owned path") + } + if strings.HasPrefix(path, routerInternalFunctionPrefix) { + return MakeValidationErr(ErrorInvalidValue, field, path, + "collides with the router-internal "+routerInternalFunctionPrefix+" prefix") + } + return nil +} + // Validate enforces the CORS spec invariants that browsers will reject // at runtime, plus the URL-shape and duration-format invariants that // surface as router-side configuration errors. Validation runs at
pkg/apis/core/v1/validation_validators_test.go+77 −0 modified@@ -137,17 +137,93 @@ func TestHTTPTriggerSpecValidate(t *testing.T) { t.Parallel() valid := HTTPTriggerSpec{ Methods: []string{"GET", "POST"}, + RelativeURL: "/api/hello", FunctionReference: FunctionReference{Type: FunctionReferenceTypeFunctionName, Name: "hello"}, } require.NoError(t, valid.Validate()) bad := HTTPTriggerSpec{ Method: "FETCH", + RelativeURL: "/api/hello", FunctionReference: FunctionReference{Type: FunctionReferenceTypeFunctionName, Name: "hello"}, } require.Error(t, bad.Validate()) } +// TestHTTPTriggerSpecValidate_Path covers the URL-path safety rules added for +// GHSA-vchh-r53j-8mpw. Keep these cases aligned with the CEL rules on +// HTTPTriggerSpec in types.go so the API server's admission decision and the +// Go-side Validate() decision (used by the CLI and the router reconciler's +// status-Condition path) stay in lockstep. +func TestHTTPTriggerSpecValidate_Path(t *testing.T) { + t.Parallel() + str := func(s string) *string { return &s } + fnRef := FunctionReference{Type: FunctionReferenceTypeFunctionName, Name: "hello"} + + for _, tc := range []struct { + name string + relativeURL string + prefix *string + wantErr bool + errSub string + }{ + // happy paths + {name: "valid relativeurl", relativeURL: "/api/hello"}, + {name: "valid prefix", prefix: str("/api/")}, + {name: "valid both set", relativeURL: "/api/hello", prefix: str("/api/")}, + {name: "literal dot-dot inside segment is allowed", relativeURL: "/api/..foo"}, + {name: "double-dot suffix in segment is allowed", relativeURL: "/api/foo..bar"}, + + // at-least-one-set + {name: "neither set", wantErr: true, errSub: "at least one"}, + {name: "empty relativeurl and empty prefix", prefix: str(""), wantErr: true, errSub: "at least one"}, + + // leading slash + {name: "no leading slash relativeurl", relativeURL: "hello", wantErr: true, errSub: "must start with '/'"}, + {name: "no leading slash prefix", prefix: str("hello"), wantErr: true, errSub: "must start with '/'"}, + + // root-only + {name: "root-only relativeurl", relativeURL: "/", wantErr: true, errSub: "root-only"}, + {name: "root-only prefix", prefix: str("/"), wantErr: true, errSub: "root-only"}, + + // `..` traversal + {name: "traversal relativeurl", relativeURL: "/api/../admin", wantErr: true, errSub: "'..'"}, + {name: "traversal prefix", prefix: str("/api/../admin"), wantErr: true, errSub: "'..'"}, + {name: "leading traversal", relativeURL: "/..", wantErr: true, errSub: "'..'"}, + {name: "trailing traversal", relativeURL: "/api/..", wantErr: true, errSub: "'..'"}, + + // router-owned exact paths + {name: "reserved /router-healthz", relativeURL: "/router-healthz", wantErr: true, errSub: "router-owned"}, + {name: "reserved /readyz", relativeURL: "/readyz", wantErr: true, errSub: "router-owned"}, + {name: "reserved /_version", relativeURL: "/_version", wantErr: true, errSub: "router-owned"}, + {name: "reserved /auth/login", relativeURL: "/auth/login", wantErr: true, errSub: "router-owned"}, + {name: "reserved path as prefix", prefix: str("/readyz"), wantErr: true, errSub: "router-owned"}, + + // router-internal /fission-function/ prefix + {name: "internal-prefix relativeurl", relativeURL: "/fission-function/ns/fn", wantErr: true, errSub: "/fission-function/"}, + {name: "internal-prefix as Prefix field", prefix: str("/fission-function/"), wantErr: true, errSub: "/fission-function/"}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + spec := HTTPTriggerSpec{ + RelativeURL: tc.relativeURL, + Prefix: tc.prefix, + FunctionReference: fnRef, + Methods: []string{"GET"}, + } + err := spec.Validate() + if tc.wantErr { + require.Error(t, err, "expected error containing %q", tc.errSub) + if tc.errSub != "" { + require.Contains(t, err.Error(), tc.errSub) + } + return + } + require.NoError(t, err) + }) + } +} + func TestIngressConfigValidate(t *testing.T) { t.Parallel() require.NoError(t, IngressConfig{Path: "/foo", Host: "*"}.Validate()) @@ -209,6 +285,7 @@ func TestCRDValidate(t *testing.T) { t.Run("httptrigger", func(t *testing.T) { h := &HTTPTrigger{ObjectMeta: meta} h.Spec.FunctionReference = FunctionReference{Type: FunctionReferenceTypeFunctionName, Name: "hello"} + h.Spec.RelativeURL = "/api/hello" require.NoError(t, h.Validate()) }) }
Vulnerability mechanics
Root cause
"The HTTPTrigger admission validation logic failed to check RelativeURL and Prefix fields, allowing them to be set to unsafe values."
Attack vector
An authenticated Kubernetes user with HTTPTrigger create permission can create an HTTPTrigger resource via `kubectl apply` or a direct Kubernetes REST API call. This bypasses the CLI-level validation for `RelativeURL` and `Prefix`. The attacker can then set these fields to values such as an empty string, a path not starting with '/', exactly '/', containing `..` traversal segments, or colliding with reserved router paths or the internal `/fission-function/` prefix [ref_id=1].
Affected code
The vulnerability lies in the `HTTPTriggerSpec.Validate()` function within `pkg/apis/core/v1/validation.go`, which silently skipped validation for `RelativeURL` and `Prefix`. These fields were only validated at the CLI level. Following the retirement of the HTTPTrigger webhook in favor of API-server CEL, these fields lacked validation rules in the CEL configuration as well [ref_id=1][ref_id=2].
What the fix does
The patch enforces path-safety invariants at both the API server's CEL admission gate and within the Go-side `HTTPTriggerSpec.Validate()` function. This ensures that the validation rules for `RelativeURL` and `Prefix` are consistent across all admission layers, including those bypassed by direct API calls. The `validateTriggerPath` helper function was updated to mirror the CEL rules, aligning CLI rejection and router reconciler status conditions with API server admission [ref_id=2][patch_id=5504356].
Preconditions
- authAuthenticated Kubernetes user with HTTPTrigger create permission.
Generated on Jun 10, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3News mentions
1- Fission Kubernetes Serverless Framework: 17 Vulnerabilities Disclosed TogetherVypr Intelligence · Jun 10, 2026