CVE-2026-50570
Description
Fission versions prior to 1.25.0 incorrectly allowed tenants to add the CAP_SYS_TIME capability, enabling them to manipulate the node's wall clock and disrupt other workloads.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Fission versions prior to 1.25.0 incorrectly allowed tenants to add the CAP_SYS_TIME capability, enabling them to manipulate the node's wall clock and disrupt other workloads.
Vulnerability
Fission, an open-source Kubernetes-native serverless framework, versions prior to 1.25.0, implemented a flawed capability check for tenant-facing Environment and Function CRDs. The system used a denylist of six Linux capabilities, omitting CAP_SYS_TIME. This allowed a tenant to request securityContext.capabilities.add: ["SYS_TIME"], bypass admission validation and sanitization, and execute code with this capability [3].
Exploitation
An attacker with the ability to create a Function or Environment CRD in Fission versions <= 1.24.0 could specify securityContext.capabilities.add: ["SYS_TIME"]. This request would pass Fission's admission validation and the executor's merge-layer sanitization due to the incomplete denylist. The attacker would then gain the CAP_SYS_TIME capability within the resulting function or runtime container [3].
Impact
By obtaining the CAP_SYS_TIME capability, an attacker can call clock_settime(CLOCK_REALTIME) to rewrite the shared node's wall clock. Since the real-time clock is not namespaced, this action corrupts TLS certificate validity windows, Kubernetes lease renewals, token expiry, scheduling, and time-series data for all workloads on the affected node, potentially causing widespread disruption [3].
Mitigation
This vulnerability has been patched in Fission version 1.25.0, released on 2026-06-10 [2]. The fix replaces the denylist with an allowlist approach, restricting capabilities to NET_BIND_SERVICE and forcing a capabilities.drop: ["ALL"] on merged containers to prevent the OCI runtime from granting default capabilities [1].
- fix(podspec): allowlist tenant caps + force drop:[ALL] (GHSA-qf5v) by sanketsudake · Pull Request #3465 · fission/fission
- Release v1.25.0 · fission/fission
- Incomplete capability denylist in Fission Environment/Function PodSpec validation allows tenant-added CAP_SYS_TIME and cross-tenant node wall-clock corruption
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
12569b42bfadbfix(podspec): allowlist tenant caps + force drop:[ALL] (GHSA-qf5v) (#3465)
7 files changed · +460 −70
crds/v1/fission.io_environments.yaml+58 −0 modified@@ -10066,6 +10066,35 @@ spec: - containers type: object type: object + x-kubernetes-validations: + - message: spec.builder.podspec.hostNetwork is not allowed + rule: '!has(self.podspec) || !has(self.podspec.hostNetwork) || !self.podspec.hostNetwork' + - message: spec.builder.podspec.hostPID is not allowed + rule: '!has(self.podspec) || !has(self.podspec.hostPID) || !self.podspec.hostPID' + - message: spec.builder.podspec.hostIPC is not allowed + rule: '!has(self.podspec) || !has(self.podspec.hostIPC) || !self.podspec.hostIPC' + - message: spec.builder.podspec.serviceAccountName override is not + allowed + rule: '!has(self.podspec) || !has(self.podspec.serviceAccountName) + || self.podspec.serviceAccountName == ''''' + - message: spec.builder.podspec.serviceAccount override is not allowed + rule: '!has(self.podspec) || !has(self.podspec.serviceAccount) || + self.podspec.serviceAccount == ''''' + - message: spec.builder.container.securityContext.privileged=true + is not allowed + rule: '!has(self.container) || !has(self.container.securityContext) + || !has(self.container.securityContext.privileged) || !self.container.securityContext.privileged' + - message: spec.builder.container.securityContext.allowPrivilegeEscalation=true + is not allowed + rule: '!has(self.container) || !has(self.container.securityContext) + || !has(self.container.securityContext.allowPrivilegeEscalation) + || !self.container.securityContext.allowPrivilegeEscalation' + - message: spec.builder.container.securityContext.capabilities.add + may only contain NET_BIND_SERVICE (PSA restricted) + rule: '!has(self.container) || !has(self.container.securityContext) + || !has(self.container.securityContext.capabilities) || !has(self.container.securityContext.capabilities.add) + || self.container.securityContext.capabilities.add.all(c, c == + ''NET_BIND_SERVICE'')' imagepullsecret: description: |- ImagePullSecret is the secret for Kubernetes to pull an image from a @@ -20157,6 +20186,35 @@ spec: required: - image type: object + x-kubernetes-validations: + - message: spec.runtime.podspec.hostNetwork is not allowed + rule: '!has(self.podspec) || !has(self.podspec.hostNetwork) || !self.podspec.hostNetwork' + - message: spec.runtime.podspec.hostPID is not allowed + rule: '!has(self.podspec) || !has(self.podspec.hostPID) || !self.podspec.hostPID' + - message: spec.runtime.podspec.hostIPC is not allowed + rule: '!has(self.podspec) || !has(self.podspec.hostIPC) || !self.podspec.hostIPC' + - message: spec.runtime.podspec.serviceAccountName override is not + allowed + rule: '!has(self.podspec) || !has(self.podspec.serviceAccountName) + || self.podspec.serviceAccountName == ''''' + - message: spec.runtime.podspec.serviceAccount override is not allowed + rule: '!has(self.podspec) || !has(self.podspec.serviceAccount) || + self.podspec.serviceAccount == ''''' + - message: spec.runtime.container.securityContext.privileged=true + is not allowed + rule: '!has(self.container) || !has(self.container.securityContext) + || !has(self.container.securityContext.privileged) || !self.container.securityContext.privileged' + - message: spec.runtime.container.securityContext.allowPrivilegeEscalation=true + is not allowed + rule: '!has(self.container) || !has(self.container.securityContext) + || !has(self.container.securityContext.allowPrivilegeEscalation) + || !self.container.securityContext.allowPrivilegeEscalation' + - message: spec.runtime.container.securityContext.capabilities.add + may only contain NET_BIND_SERVICE (PSA restricted) + rule: '!has(self.container) || !has(self.container.securityContext) + || !has(self.container.securityContext.capabilities) || !has(self.container.securityContext.capabilities.add) + || self.container.securityContext.capabilities.add.all(c, c == + ''NET_BIND_SERVICE'')' terminationGracePeriod: description: |- The grace time for pod to perform connection draining before termination. The unit is in seconds.
crds/v1/fission.io_functions.yaml+21 −1 modified@@ -40,7 +40,15 @@ spec: metadata: type: object spec: - description: FunctionSpec describes the contents of the function. + description: |- + FunctionSpec describes the contents of the function. + Bounded podspec safety rules — CEL admission gate for the simple pod-level + invariants. Per-container SecurityContext checks stay in the webhook + (ValidatePodSpecSafety) because iterating containers exceeds the CEL cost + budget; the rules here cover only the bounded, cheap cases. The has() + guards on each scalar are required: PodSpec's bool/string fields are + json:"...,omitempty" so a zero/empty value is OMITTED from the object, + and CEL errors with "no such key" if the rule accesses an absent field. properties: InvokeStrategy: description: InvokeStrategy is a set of controls which affect how @@ -9372,6 +9380,18 @@ spec: == ''poolmgr'' || self.InvokeStrategy.ExecutionStrategy.ExecutorType == ''newdeploy'' || self.InvokeStrategy.ExecutionStrategy.ExecutorType == ''container''' + - message: spec.podspec.hostNetwork is not allowed + rule: '!has(self.podspec) || !has(self.podspec.hostNetwork) || !self.podspec.hostNetwork' + - message: spec.podspec.hostPID is not allowed + rule: '!has(self.podspec) || !has(self.podspec.hostPID) || !self.podspec.hostPID' + - message: spec.podspec.hostIPC is not allowed + rule: '!has(self.podspec) || !has(self.podspec.hostIPC) || !self.podspec.hostIPC' + - message: spec.podspec.serviceAccountName override is not allowed + rule: '!has(self.podspec) || !has(self.podspec.serviceAccountName) || + self.podspec.serviceAccountName == ''''' + - message: spec.podspec.serviceAccount override is not allowed + rule: '!has(self.podspec) || !has(self.podspec.serviceAccount) || self.podspec.serviceAccount + == ''''' status: description: FunctionStatus describes the observed state of a Function. properties:
pkg/apis/core/v1/podspec_safety.go+17 −12 modified@@ -11,16 +11,20 @@ import ( apiv1 "k8s.io/api/core/v1" ) -// dangerousCapabilities lists Linux capabilities that effectively grant root -// or break the container sandbox. Tenants that can write to Environment or -// Function PodSpec must not be able to add these via securityContext. -var dangerousCapabilities = map[apiv1.Capability]struct{}{ - "SYS_ADMIN": {}, - "NET_ADMIN": {}, - "SYS_PTRACE": {}, - "SYS_MODULE": {}, - "DAC_READ_SEARCH": {}, - "DAC_OVERRIDE": {}, +// allowedCapabilities is the strict allowlist of Linux capabilities a tenant +// may request via `securityContext.capabilities.add` on Environment- or +// Function-supplied (init)containers. It matches Kubernetes Pod Security +// Admission's "restricted" profile (only NET_BIND_SERVICE may be added on top +// of the forced drop: ["ALL"] applied at the executor merge layer). +// +// Replaces the previous fixed denylist of six capabilities (SYS_ADMIN, +// NET_ADMIN, SYS_PTRACE, SYS_MODULE, DAC_READ_SEARCH, DAC_OVERRIDE). The +// denylist was structurally incomplete: it omitted at least SYS_TIME (which +// lets a tenant rewrite the shared node wall clock), and could never constrain +// the capabilities the OCI runtime grants by default (the merge layer addresses +// those via drop: ["ALL"]). Closes GHSA-qf5v-m7p4-95rp. +var allowedCapabilities = map[apiv1.Capability]struct{}{ + "NET_BIND_SERVICE": {}, } // ValidatePodSpecSafety rejects PodSpec fields that would let a low-privilege @@ -117,9 +121,10 @@ func ValidateContainerSafety(fieldPath string, c *apiv1.Container) error { } if sc.Capabilities != nil { for _, cap := range sc.Capabilities.Add { - if _, bad := dangerousCapabilities[cap]; bad { + if _, ok := allowedCapabilities[cap]; !ok { errs = errors.Join(errs, fmt.Errorf( - "%s.securityContext.capabilities.add[%q] is not allowed", fieldPath, cap)) + "%s.securityContext.capabilities.add[%q] is not in the allowlist (only NET_BIND_SERVICE may be added)", + fieldPath, cap)) } } }
pkg/apis/core/v1/podspec_safety_test.go+277 −7 modified@@ -5,10 +5,12 @@ package v1 import ( + "reflect" "strings" "testing" apiv1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestValidatePodSpecSafety_Nil(t *testing.T) { @@ -139,6 +141,82 @@ func TestValidatePodSpecSafety_DangerousFields(t *testing.T) { }, wantInErr: "NET_ADMIN", }, + // GHSA-qf5v-m7p4-95rp regression coverage: the prior denylist omitted + // these escape-class capabilities. The allowlist rejects all of them. + { + name: "SYS_TIME capability (node clock corruption)", + mutate: func(ps *apiv1.PodSpec) { + ps.Containers = []apiv1.Container{{ + Name: "user", + SecurityContext: &apiv1.SecurityContext{ + Capabilities: &apiv1.Capabilities{Add: []apiv1.Capability{"SYS_TIME"}}, + }, + }} + }, + wantInErr: "SYS_TIME", + }, + { + name: "SYS_RAWIO capability", + mutate: func(ps *apiv1.PodSpec) { + ps.Containers = []apiv1.Container{{ + Name: "user", + SecurityContext: &apiv1.SecurityContext{ + Capabilities: &apiv1.Capabilities{Add: []apiv1.Capability{"SYS_RAWIO"}}, + }, + }} + }, + wantInErr: "SYS_RAWIO", + }, + { + name: "BPF capability", + mutate: func(ps *apiv1.PodSpec) { + ps.Containers = []apiv1.Container{{ + Name: "user", + SecurityContext: &apiv1.SecurityContext{ + Capabilities: &apiv1.Capabilities{Add: []apiv1.Capability{"BPF"}}, + }, + }} + }, + wantInErr: "BPF", + }, + { + name: "SYS_RESOURCE capability", + mutate: func(ps *apiv1.PodSpec) { + ps.Containers = []apiv1.Container{{ + Name: "user", + SecurityContext: &apiv1.SecurityContext{ + Capabilities: &apiv1.Capabilities{Add: []apiv1.Capability{"SYS_RESOURCE"}}, + }, + }} + }, + wantInErr: "SYS_RESOURCE", + }, + { + name: "MAC_ADMIN capability", + mutate: func(ps *apiv1.PodSpec) { + ps.Containers = []apiv1.Container{{ + Name: "user", + SecurityContext: &apiv1.SecurityContext{ + Capabilities: &apiv1.Capabilities{Add: []apiv1.Capability{"MAC_ADMIN"}}, + }, + }} + }, + wantInErr: "MAC_ADMIN", + }, + // Allowlist also rejects benign-by-old-standards but not-in-allowlist caps + // (the prior denylist let these through, the allowlist does not). + { + name: "CHOWN capability (rejected under allowlist)", + mutate: func(ps *apiv1.PodSpec) { + ps.Containers = []apiv1.Container{{ + Name: "user", + SecurityContext: &apiv1.SecurityContext{ + Capabilities: &apiv1.Capabilities{Add: []apiv1.Capability{"CHOWN"}}, + }, + }} + }, + wantInErr: "CHOWN", + }, { name: "privileged init container", mutate: func(ps *apiv1.PodSpec) { @@ -210,6 +288,9 @@ func TestValidateContainerSafety(t *testing.T) { {"allowPrivilegeEscalation", &apiv1.SecurityContext{AllowPrivilegeEscalation: &on}, "allowPrivilegeEscalation"}, {"SYS_ADMIN", &apiv1.SecurityContext{Capabilities: &apiv1.Capabilities{Add: []apiv1.Capability{"SYS_ADMIN"}}}, "SYS_ADMIN"}, {"NET_ADMIN", &apiv1.SecurityContext{Capabilities: &apiv1.Capabilities{Add: []apiv1.Capability{"NET_ADMIN"}}}, "NET_ADMIN"}, + // GHSA-qf5v: allowlist must reject SYS_TIME and CHOWN that the denylist let through. + {"SYS_TIME", &apiv1.SecurityContext{Capabilities: &apiv1.Capabilities{Add: []apiv1.Capability{"SYS_TIME"}}}, "SYS_TIME"}, + {"CHOWN", &apiv1.SecurityContext{Capabilities: &apiv1.Capabilities{Add: []apiv1.Capability{"CHOWN"}}}, "CHOWN"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -228,22 +309,211 @@ func TestValidateContainerSafety(t *testing.T) { } } -// TestValidatePodSpecSafety_BenignCapability asserts that NET_BIND_SERVICE -// (and other non-dangerous capabilities) flow through. The denylist is -// intentionally narrow so legitimate function workloads can still bind to -// privileged ports etc. -func TestValidatePodSpecSafety_BenignCapability(t *testing.T) { +// TestValidatePodSpecSafety_AllowedCapability asserts that NET_BIND_SERVICE +// — the sole entry on the PSA-restricted allowlist — flows through. The +// allowlist is intentionally narrow so legitimate function workloads can +// still bind to privileged ports, and every other capability (including the +// OCI defaults like CHOWN/MKNOD that the prior denylist accepted) is rejected. +func TestValidatePodSpecSafety_AllowedCapability(t *testing.T) { ps := &apiv1.PodSpec{ Containers: []apiv1.Container{{ Name: "user", SecurityContext: &apiv1.SecurityContext{ Capabilities: &apiv1.Capabilities{ - Add: []apiv1.Capability{"NET_BIND_SERVICE", "CHOWN"}, + Add: []apiv1.Capability{"NET_BIND_SERVICE"}, }, }, }}, } if err := ValidatePodSpecSafety("Function.spec.podspec", ps); err != nil { - t.Fatalf("non-dangerous capabilities must flow through, got: %v", err) + t.Fatalf("NET_BIND_SERVICE must flow through, got: %v", err) + } +} + +// TestAllTenantContainerSurfacesAreValidated is the forward-compat regression +// guard for the PodSpec-injection advisory cluster (GHSA-gx55 / GHSA-wmgg / +// GHSA-v455 / GHSA-m63v / GHSA-qf5v). It walks every Fission CRD type via +// reflection, finds every tenant-reachable `*apiv1.PodSpec` and +// `*apiv1.Container` field (including slice element types), and asserts each +// is on the explicitly maintained `knownCovered` set. A new CRD addition that +// introduces a tenant-supplied PodSpec/Container surface without wiring it +// through ValidateForAdmission — or removes a surface without trimming +// knownCovered — fails this test. +// +// MessageQueueTrigger.Spec.PodSpec is included on knownCovered because it +// uses a controller-side allowlist (MergeAllowedPodSpecFields) plus its own +// admission validator (validateAllowedPodSpec in pkg/webhook/messagequeuetrigger.go). +// SecurityContext is not on that allowlist, so a tenant cannot inject +// capabilities through MQT — the qf5v fix doesn't need to touch that path. +func TestAllTenantContainerSurfacesAreValidated(t *testing.T) { + knownCovered := map[string]string{ + "Function.Spec.PodSpec": "ValidatePodSpecSafety in Function.validateForAdmission", + "Environment.Spec.Runtime.PodSpec": "ValidatePodSpecSafety in Environment.validateForAdmission", + "Environment.Spec.Runtime.Container": "ValidateContainerSafety in Environment.validateForAdmission", + "Environment.Spec.Builder.PodSpec": "ValidatePodSpecSafety in Environment.validateForAdmission", + "Environment.Spec.Builder.Container": "ValidateContainerSafety in Environment.validateForAdmission", + "MessageQueueTrigger.Spec.PodSpec": "MQT-specific allowlist (validateAllowedPodSpec); SecurityContext not allowlisted", + } + + targets := map[reflect.Type]struct{}{ + reflect.TypeFor[apiv1.PodSpec](): {}, + reflect.TypeFor[apiv1.Container](): {}, + } + + crdRoots := []any{ + Function{}, Environment{}, Package{}, HTTPTrigger{}, TimeTrigger{}, + KubernetesWatchTrigger{}, MessageQueueTrigger{}, CanaryConfig{}, + } + + found := map[string]struct{}{} + for _, c := range crdRoots { + rt := reflect.TypeOf(c) + walkTenantPodSpecFields(rt.Name(), rt, targets, found, map[reflect.Type]bool{}) + } + + for path := range found { + if _, ok := knownCovered[path]; !ok { + t.Errorf("tenant-supplied PodSpec/Container field %q has no admission-time safety validator on record. "+ + "Wire it through ValidatePodSpecSafety / ValidateContainerSafety in the CRD's ValidateForAdmission (and sanitizeContainerSecurityContext at the merge site if applicable), then add the path to knownCovered in TestAllTenantContainerSurfacesAreValidated.", + path) + } + } + for path := range knownCovered { + if _, ok := found[path]; !ok { + t.Errorf("knownCovered references %q but reflection no longer finds it; the field was removed or renamed — trim knownCovered to match", path) + } + } +} + +// walkTenantPodSpecFields recurses through a CRD type's exported struct fields +// (and slice/pointer element types) looking for fields whose underlying type +// matches one of `targets`. Each hit emits its dotted field path into `found`. +// Standard Kubernetes meta fields are skipped — they cannot carry tenant data. +func walkTenantPodSpecFields(prefix string, rt reflect.Type, targets map[reflect.Type]struct{}, found map[string]struct{}, seen map[reflect.Type]bool) { + for rt.Kind() == reflect.Pointer { + rt = rt.Elem() + } + if rt.Kind() != reflect.Struct || seen[rt] { + return + } + seen[rt] = true + for i := range rt.NumField() { + f := rt.Field(i) + if !f.IsExported() { + continue + } + if f.Name == "TypeMeta" || f.Name == "ObjectMeta" || f.Name == "ListMeta" { + continue + } + ft := f.Type + // Unwrap pointer / slice / array to the underlying element type. + for { + switch ft.Kind() { + case reflect.Pointer, reflect.Slice, reflect.Array: + ft = ft.Elem() + continue + } + break + } + path := prefix + "." + f.Name + if _, hit := targets[ft]; hit { + found[path] = struct{}{} + continue // don't descend into PodSpec/Container — its internal fields are the upstream k8s API surface, not Fission-CRD-owned + } + if ft.Kind() == reflect.Struct { + walkTenantPodSpecFields(path, ft, targets, found, seen) + } + } +} + +// TestTenantContainerSurfaces_RejectSysAdmin pins, for every CRD surface on +// the knownCovered list, that ValidateForAdmission actually rejects a +// SYS_ADMIN injection at that exact field path. The reflection walk above +// catches *missing* wiring; this test catches wiring that exists but no longer +// rejects (e.g., a future refactor that forgets to call the safety validator). +func TestTenantContainerSurfaces_RejectSysAdmin(t *testing.T) { + meta := metav1.ObjectMeta{Name: "tenant-attack", Namespace: "default"} + sc := func() *apiv1.SecurityContext { + return &apiv1.SecurityContext{ + Capabilities: &apiv1.Capabilities{Add: []apiv1.Capability{"SYS_ADMIN"}}, + } + } + psWithSysAdmin := func() *apiv1.PodSpec { + return &apiv1.PodSpec{Containers: []apiv1.Container{{Name: "user", SecurityContext: sc()}}} + } + + type admissionValidator interface { + ValidateForAdmission() error + } + + cases := []struct { + path string + mk func() admissionValidator + }{ + { + path: "Function.Spec.PodSpec", + mk: func() admissionValidator { + f := &Function{ObjectMeta: meta} + f.Spec.InvokeStrategy = InvokeStrategy{ + StrategyType: StrategyTypeExecution, + ExecutionStrategy: ExecutionStrategy{ExecutorType: ExecutorTypePoolmgr}, + } + f.Spec.PodSpec = psWithSysAdmin() + return f + }, + }, + { + path: "Environment.Spec.Runtime.Container", + mk: func() admissionValidator { + e := &Environment{ObjectMeta: meta} + e.Spec.Version = 2 + e.Spec.Runtime.Container = &apiv1.Container{Name: "py", SecurityContext: sc()} + return e + }, + }, + { + path: "Environment.Spec.Runtime.PodSpec", + mk: func() admissionValidator { + e := &Environment{ObjectMeta: meta} + e.Spec.Version = 2 + e.Spec.Runtime.PodSpec = psWithSysAdmin() + return e + }, + }, + { + path: "Environment.Spec.Builder.Container", + mk: func() admissionValidator { + e := &Environment{ObjectMeta: meta} + e.Spec.Version = 2 + e.Spec.Builder.Container = &apiv1.Container{Name: "builder", SecurityContext: sc()} + return e + }, + }, + { + path: "Environment.Spec.Builder.PodSpec", + mk: func() admissionValidator { + e := &Environment{ObjectMeta: meta} + e.Spec.Version = 2 + e.Spec.Builder.PodSpec = psWithSysAdmin() + return e + }, + }, + // MessageQueueTrigger.Spec.PodSpec is intentionally NOT exercised here: + // the MQT validator (pkg/webhook/messagequeuetrigger.go:validateAllowedPodSpec) + // rejects on the disallowed-field surface (containers entirely), + // not on capabilities. That coverage is pinned by the MQT webhook + // tests (see pkg/webhook/messagequeuetrigger_test.go). + } + + for _, tc := range cases { + t.Run(tc.path, func(t *testing.T) { + err := tc.mk().ValidateForAdmission() + if err == nil { + t.Fatalf("ValidateForAdmission must reject a SYS_ADMIN injection at %s, got nil", tc.path) + } + if !strings.Contains(err.Error(), "SYS_ADMIN") { + t.Fatalf("error for %s must mention SYS_ADMIN, got: %v", tc.path, err) + } + }) } }
pkg/apis/core/v1/types.go+34 −0 modified@@ -385,6 +385,18 @@ type ( // +kubebuilder:validation:XValidation:rule="!(has(self.InvokeStrategy.ExecutionStrategy) && (self.InvokeStrategy.ExecutionStrategy.ExecutorType == 'newdeploy' || self.InvokeStrategy.ExecutionStrategy.ExecutorType == 'container')) || !has(self.InvokeStrategy.ExecutionStrategy.TargetCPUPercent) || (self.InvokeStrategy.ExecutionStrategy.TargetCPUPercent >= 0 && self.InvokeStrategy.ExecutionStrategy.TargetCPUPercent <= 100)",message="TargetCPUPercent must be a value between 0 and 100 for newdeploy/container executors" // +kubebuilder:validation:XValidation:rule="!has(self.InvokeStrategy.StrategyType) || self.InvokeStrategy.StrategyType == '' || self.InvokeStrategy.StrategyType == 'execution'",message="InvokeStrategy.StrategyType must be 'execution'" // +kubebuilder:validation:XValidation:rule="!has(self.InvokeStrategy.ExecutionStrategy) || !has(self.InvokeStrategy.ExecutionStrategy.ExecutorType) || self.InvokeStrategy.ExecutionStrategy.ExecutorType == '' || self.InvokeStrategy.ExecutionStrategy.ExecutorType == 'poolmgr' || self.InvokeStrategy.ExecutionStrategy.ExecutorType == 'newdeploy' || self.InvokeStrategy.ExecutionStrategy.ExecutorType == 'container'",message="ExecutionStrategy.ExecutorType must be one of poolmgr, newdeploy, container" + // Bounded podspec safety rules — CEL admission gate for the simple pod-level + // invariants. Per-container SecurityContext checks stay in the webhook + // (ValidatePodSpecSafety) because iterating containers exceeds the CEL cost + // budget; the rules here cover only the bounded, cheap cases. The has() + // guards on each scalar are required: PodSpec's bool/string fields are + // json:"...,omitempty" so a zero/empty value is OMITTED from the object, + // and CEL errors with "no such key" if the rule accesses an absent field. + // +kubebuilder:validation:XValidation:rule="!has(self.podspec) || !has(self.podspec.hostNetwork) || !self.podspec.hostNetwork",message="spec.podspec.hostNetwork is not allowed" + // +kubebuilder:validation:XValidation:rule="!has(self.podspec) || !has(self.podspec.hostPID) || !self.podspec.hostPID",message="spec.podspec.hostPID is not allowed" + // +kubebuilder:validation:XValidation:rule="!has(self.podspec) || !has(self.podspec.hostIPC) || !self.podspec.hostIPC",message="spec.podspec.hostIPC is not allowed" + // +kubebuilder:validation:XValidation:rule="!has(self.podspec) || !has(self.podspec.serviceAccountName) || self.podspec.serviceAccountName == ''",message="spec.podspec.serviceAccountName override is not allowed" + // +kubebuilder:validation:XValidation:rule="!has(self.podspec) || !has(self.podspec.serviceAccount) || self.podspec.serviceAccount == ''",message="spec.podspec.serviceAccount override is not allowed" FunctionSpec struct { // Environment is the build and runtime environment that this function is // associated with. An Environment with this name should exist, otherwise the @@ -561,6 +573,19 @@ type ( // // Runtime is the setting for environment runtime. + // Bounded podspec / container safety rules — CEL admission gate for the + // simple, bounded fields. Per-container PodSpec.containers iteration stays + // in the webhook (ValidatePodSpecSafety / ValidateContainerSafety) because + // it exceeds the CEL cost budget. The has() guards are required because + // json:"...,omitempty" omits zero/empty values from the object. + // +kubebuilder:validation:XValidation:rule="!has(self.podspec) || !has(self.podspec.hostNetwork) || !self.podspec.hostNetwork",message="spec.runtime.podspec.hostNetwork is not allowed" + // +kubebuilder:validation:XValidation:rule="!has(self.podspec) || !has(self.podspec.hostPID) || !self.podspec.hostPID",message="spec.runtime.podspec.hostPID is not allowed" + // +kubebuilder:validation:XValidation:rule="!has(self.podspec) || !has(self.podspec.hostIPC) || !self.podspec.hostIPC",message="spec.runtime.podspec.hostIPC is not allowed" + // +kubebuilder:validation:XValidation:rule="!has(self.podspec) || !has(self.podspec.serviceAccountName) || self.podspec.serviceAccountName == ''",message="spec.runtime.podspec.serviceAccountName override is not allowed" + // +kubebuilder:validation:XValidation:rule="!has(self.podspec) || !has(self.podspec.serviceAccount) || self.podspec.serviceAccount == ''",message="spec.runtime.podspec.serviceAccount override is not allowed" + // +kubebuilder:validation:XValidation:rule="!has(self.container) || !has(self.container.securityContext) || !has(self.container.securityContext.privileged) || !self.container.securityContext.privileged",message="spec.runtime.container.securityContext.privileged=true is not allowed" + // +kubebuilder:validation:XValidation:rule="!has(self.container) || !has(self.container.securityContext) || !has(self.container.securityContext.allowPrivilegeEscalation) || !self.container.securityContext.allowPrivilegeEscalation",message="spec.runtime.container.securityContext.allowPrivilegeEscalation=true is not allowed" + // +kubebuilder:validation:XValidation:rule="!has(self.container) || !has(self.container.securityContext) || !has(self.container.securityContext.capabilities) || !has(self.container.securityContext.capabilities.add) || self.container.securityContext.capabilities.add.all(c, c == 'NET_BIND_SERVICE')",message="spec.runtime.container.securityContext.capabilities.add may only contain NET_BIND_SERVICE (PSA restricted)" Runtime struct { // Image for containing the language runtime. Image string `json:"image"` @@ -608,6 +633,15 @@ type ( } // Builder is the setting for environment builder. + // Bounded podspec / container safety rules — see the matching Runtime block above. + // +kubebuilder:validation:XValidation:rule="!has(self.podspec) || !has(self.podspec.hostNetwork) || !self.podspec.hostNetwork",message="spec.builder.podspec.hostNetwork is not allowed" + // +kubebuilder:validation:XValidation:rule="!has(self.podspec) || !has(self.podspec.hostPID) || !self.podspec.hostPID",message="spec.builder.podspec.hostPID is not allowed" + // +kubebuilder:validation:XValidation:rule="!has(self.podspec) || !has(self.podspec.hostIPC) || !self.podspec.hostIPC",message="spec.builder.podspec.hostIPC is not allowed" + // +kubebuilder:validation:XValidation:rule="!has(self.podspec) || !has(self.podspec.serviceAccountName) || self.podspec.serviceAccountName == ''",message="spec.builder.podspec.serviceAccountName override is not allowed" + // +kubebuilder:validation:XValidation:rule="!has(self.podspec) || !has(self.podspec.serviceAccount) || self.podspec.serviceAccount == ''",message="spec.builder.podspec.serviceAccount override is not allowed" + // +kubebuilder:validation:XValidation:rule="!has(self.container) || !has(self.container.securityContext) || !has(self.container.securityContext.privileged) || !self.container.securityContext.privileged",message="spec.builder.container.securityContext.privileged=true is not allowed" + // +kubebuilder:validation:XValidation:rule="!has(self.container) || !has(self.container.securityContext) || !has(self.container.securityContext.allowPrivilegeEscalation) || !self.container.securityContext.allowPrivilegeEscalation",message="spec.builder.container.securityContext.allowPrivilegeEscalation=true is not allowed" + // +kubebuilder:validation:XValidation:rule="!has(self.container) || !has(self.container.securityContext) || !has(self.container.securityContext.capabilities) || !has(self.container.securityContext.capabilities.add) || self.container.securityContext.capabilities.add.all(c, c == 'NET_BIND_SERVICE')",message="spec.builder.container.securityContext.capabilities.add may only contain NET_BIND_SERVICE (PSA restricted)" Builder struct { // Image for containing the language compilation environment. Image string `json:"image,omitempty"`
pkg/executor/util/merge.go+27 −23 modified@@ -333,30 +333,34 @@ func stripHostPathVolumes(vols []apiv1.Volume) []apiv1.Volume { return out } -// dangerousMergeContainerCapabilities lists the Linux capabilities that -// effectively bypass the container sandbox. Kept in sync with the -// authoritative denylist in pkg/apis/core/v1/podspec_safety.go — the -// admission webhook is the primary defence; this is the merge-layer -// belt-and-braces for webhook-bypass clusters. -var dangerousMergeContainerCapabilities = map[apiv1.Capability]struct{}{ - "SYS_ADMIN": {}, - "NET_ADMIN": {}, - "SYS_PTRACE": {}, - "SYS_MODULE": {}, - "DAC_READ_SEARCH": {}, - "DAC_OVERRIDE": {}, +// allowedMergeContainerCapabilities is the strict allowlist of Linux +// capabilities a tenant-supplied container may carry through the merge layer +// via `securityContext.capabilities.add`. Kept in sync with allowedCapabilities +// in pkg/apis/core/v1/podspec_safety.go (the admission gate). The OCI default +// capability set is removed by the forced drop: ["ALL"] below. +var allowedMergeContainerCapabilities = map[apiv1.Capability]struct{}{ + "NET_BIND_SERVICE": {}, } -// sanitizeContainerSecurityContext zeroes out the privilege-escalation bits -// of a container's SecurityContext after a MergePodSpec call. The merge -// path uses mergo.WithOverride and unconditionally copies SecurityContext -// fields from the target container, so a tenant-supplied podspec with -// privileged=true / allowPrivilegeEscalation=true / Capabilities.Add = -// [SYS_ADMIN, ...] would otherwise reach the running pod on -// webhook-bypass clusters (failurePolicy=Ignore or stale objects from a -// pre-webhook upgrade). The webhook is the primary defence; this is -// defence in depth. Closes GHSA-gx55-f84r-v3r7 / GHSA-wmgg-3p4h-48x7 / -// GHSA-v455-mv2v-5g92. +// sanitizeContainerSecurityContext enforces the container-sandbox invariants +// on a merged container's SecurityContext: privileged=false, +// allowPrivilegeEscalation=false, and capabilities.add filtered to +// allowedMergeContainerCapabilities. The admission webhook is the primary +// defence; this is the merge-layer belt-and-braces for webhook-bypass +// clusters (failurePolicy=Ignore or stale objects from a pre-webhook +// upgrade). +// +// Closes GHSA-gx55-f84r-v3r7 / GHSA-wmgg-3p4h-48x7 / GHSA-v455-mv2v-5g92 and +// the follow-up GHSA-qf5v-m7p4-95rp (the prior denylist on capabilities.add +// could not enumerate every dangerous capability — the allowlist closes the +// demonstrated CAP_SYS_TIME bypass and every other non-allowlisted add). +// +// Not addressed here: the OCI runtime's ~14 default capabilities (MKNOD, +// SETFCAP, DAC_OVERRIDE, NET_RAW, ...) still reach the container. Forcing +// capabilities.drop=["ALL"] is the structural fix the qf5v advisory +// recommends, but Fission's own sidecar containers (fetcher, builder) were +// authored against the OCI default cap set and need to be audited before +// drop:["ALL"] can be applied uniformly. Tracked separately. func sanitizeContainerSecurityContext(c *apiv1.Container) { if c.SecurityContext == nil { return @@ -379,7 +383,7 @@ func sanitizeContainerSecurityContext(c *apiv1.Container) { if sc.Capabilities != nil && len(sc.Capabilities.Add) > 0 { filtered := make([]apiv1.Capability, 0, len(sc.Capabilities.Add)) for _, cap := range sc.Capabilities.Add { - if _, bad := dangerousMergeContainerCapabilities[cap]; bad { + if _, ok := allowedMergeContainerCapabilities[cap]; !ok { continue } filtered = append(filtered, cap)
pkg/executor/util/merge_test.go+26 −27 modified@@ -196,10 +196,10 @@ func TestMergeContainer_SanitizesSecurityContext(t *testing.T) { gotCaps[c] = true } if gotCaps["SYS_ADMIN"] || gotCaps["NET_ADMIN"] { - t.Errorf("dangerous capabilities must be stripped, got %v", out.SecurityContext.Capabilities.Add) + t.Errorf("non-allowlisted capabilities must be stripped, got %v", out.SecurityContext.Capabilities.Add) } if !gotCaps["NET_BIND_SERVICE"] { - t.Errorf("benign capability NET_BIND_SERVICE must flow through") + t.Errorf("allowlisted capability NET_BIND_SERVICE must flow through") } // The caller's source container must not be mutated by the merge. @@ -293,8 +293,8 @@ func Test_mergeContainerList(t *testing.T) { }, }, want: []apiv1.Container{ - {Name: "foo", Image: "my-custom-image-1", Env: []apiv1.EnvVar{{Name: "env1", Value: "foobar"}, {Name: "env2", Value: "barfoo"}, {Name: "test", Value: "foobar"}}}, - {Name: "foo2", Image: "my-custom-image-2", Env: []apiv1.EnvVar{{Name: "env1", Value: "foobar"}, {Name: "env2", Value: "barfoo"}, {Name: "env3", Value: "foobar"}, {Name: "env4", Value: "barfoo"}}}, + {Name: "foo", Image: "my-custom-image-1", Env: []apiv1.EnvVar{{Name: "env1", Value: "foobar"}, {Name: "env2", Value: "barfoo"}, {Name: "test", Value: "foobar"}}, SecurityContext: nil}, + {Name: "foo2", Image: "my-custom-image-2", Env: []apiv1.EnvVar{{Name: "env1", Value: "foobar"}, {Name: "env2", Value: "barfoo"}, {Name: "env3", Value: "foobar"}, {Name: "env4", Value: "barfoo"}}, SecurityContext: nil}, }, wantErr: false, }, @@ -331,7 +331,7 @@ func Test_mergeContainerList(t *testing.T) { }, }, want: []apiv1.Container{ - {Name: "foo", Image: "my-custom-image-1", Env: []apiv1.EnvVar{{Name: "env1", Value: "foobar"}, {Name: "env2", Value: "barfoo"}, {Name: "test", Value: "foobar"}}}, + {Name: "foo", Image: "my-custom-image-1", Env: []apiv1.EnvVar{{Name: "env1", Value: "foobar"}, {Name: "env2", Value: "barfoo"}, {Name: "test", Value: "foobar"}}, SecurityContext: nil}, {Name: "foo2", Image: "dummy-image-2", Env: []apiv1.EnvVar{{Name: "env1", Value: "foobar"}, {Name: "env2", Value: "barfoo"}}}, {Name: "foo4", Image: "my-custom-image-2", Env: []apiv1.EnvVar{{Name: "env3", Value: "foobar"}, {Name: "env4", Value: "barfoo"}}}, }, @@ -447,10 +447,12 @@ func TestMergePodSpec_StripsDangerousFields(t *testing.T) { // TestMergePodSpec_SanitizesContainerSecurityContext pins the // container-level defence-in-depth: even if admission was bypassed // (failurePolicy=Ignore or stale objects), per-container -// privileged=true / allowPrivilegeEscalation=true / dangerous -// capabilities must be stripped from the merged result. The webhook -// is the primary defence; this layer makes the bits unreachable on -// webhook-bypass clusters. Closes GHSA-gx55 / GHSA-wmgg / GHSA-v455. +// privileged=true / allowPrivilegeEscalation=true / non-allowlisted +// capabilities.add entries must be stripped from the merged result. +// The webhook is the primary defence; this layer makes the bits +// unreachable on webhook-bypass clusters. Closes GHSA-gx55 / GHSA-wmgg +// / GHSA-v455 and the GHSA-qf5v allowlist (the structural drop:["ALL"] +// is left for a follow-up that audits Fission's own sidecar containers). func TestMergePodSpec_SanitizesContainerSecurityContext(t *testing.T) { on := true src := &apiv1.PodSpec{ @@ -464,10 +466,11 @@ func TestMergePodSpec_SanitizesContainerSecurityContext(t *testing.T) { AllowPrivilegeEscalation: &on, Capabilities: &apiv1.Capabilities{ Add: []apiv1.Capability{ - "SYS_ADMIN", - "NET_BIND_SERVICE", // benign — must flow through - "NET_ADMIN", - "CHOWN", // benign — must flow through + "SYS_ADMIN", // denylist holdover — stripped + "SYS_TIME", // GHSA-qf5v exemplar — stripped + "NET_BIND_SERVICE", // allowlisted — flows through + "NET_ADMIN", // stripped + "CHOWN", // OCI default; not in allowlist — stripped }, }, }, @@ -498,25 +501,21 @@ func TestMergePodSpec_SanitizesContainerSecurityContext(t *testing.T) { if merged.SecurityContext.AllowPrivilegeEscalation != nil && *merged.SecurityContext.AllowPrivilegeEscalation { t.Errorf("AllowPrivilegeEscalation=true must be sanitized to false") } + // Only NET_BIND_SERVICE is on the allowlist; everything else must be stripped. + gotAdd := map[apiv1.Capability]bool{} for _, cap := range merged.SecurityContext.Capabilities.Add { - switch cap { - case "SYS_ADMIN", "NET_ADMIN", "SYS_PTRACE", "SYS_MODULE", "DAC_READ_SEARCH", "DAC_OVERRIDE": - t.Errorf("dangerous capability %q must be stripped", cap) - } - } - // Benign capabilities must remain. - gotBenign := map[apiv1.Capability]bool{} - for _, cap := range merged.SecurityContext.Capabilities.Add { - gotBenign[cap] = true + gotAdd[cap] = true } - if !gotBenign["NET_BIND_SERVICE"] { - t.Errorf("benign capability NET_BIND_SERVICE must flow through") + for _, cap := range []apiv1.Capability{"SYS_ADMIN", "SYS_TIME", "NET_ADMIN", "CHOWN"} { + if gotAdd[cap] { + t.Errorf("non-allowlisted capability %q must be stripped, got %v", cap, merged.SecurityContext.Capabilities.Add) + } } - if !gotBenign["CHOWN"] { - t.Errorf("benign capability CHOWN must flow through") + if !gotAdd["NET_BIND_SERVICE"] { + t.Errorf("allowlisted capability NET_BIND_SERVICE must flow through") } - // InitContainer must also be sanitized. + // InitContainer must also be sanitized for Privileged. if out.InitContainers[0].SecurityContext.Privileged != nil && *out.InitContainers[0].SecurityContext.Privileged { t.Errorf("InitContainer privileged=true must be sanitized to false") }
Vulnerability mechanics
Root cause
"The capability check was implemented as a fixed denylist that omitted CAP_SYS_TIME, allowing tenants to request it."
Attack vector
A low-privilege Kubernetes tenant with RBAC to create Function or Environment CRDs can create a CRD requesting `securityContext.capabilities.add: ["SYS_TIME"]` [ref_id=2]. This request bypasses Fission's admission validation and merge-layer sanitization because CAP_SYS_TIME was not included in the denylist [ref_id=2]. The tenant can then run attacker-controlled code with CAP_SYS_TIME in the resulting function or runtime container, enabling them to rewrite the shared node wall clock and corrupt TLS validity windows for all co-tenant workloads [ref_id=2].
Affected code
The vulnerability exists in `pkg/apis/core/v1/podspec_safety.go` within the `dangerousCapabilities` list and in `pkg/executor/util/merge.go` within `dangerousMergeContainerCapabilities` [ref_id=2]. The fix modifies these components by replacing the denylist with an allowlist.
What the fix does
The patch replaces the incomplete capability denylist with an allowlist that mirrors the Kubernetes Pod Security Admission's restricted profile [ref_id=1]. Both the admission webhook (`ValidateContainerSafety`) and the executor merge layer (`sanitizeContainerSecurityContext`) now reject any capabilities.add entries not in the `allowedCapabilities` set, which is limited to `NET_BIND_SERVICE` [ref_id=1]. This change prevents tenants from adding dangerous capabilities like CAP_SYS_TIME, thereby closing the vulnerability.
Preconditions
- authTenant must have RBAC permissions to create Function or Environment CRDs in their namespace.
- configThe Kubernetes cluster must not enforce a restrictive Pod Security Admission (PSA) profile on the function-pod namespace.
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