VYPR
Critical severity9.9NVD Advisory· Published Jun 10, 2026· Updated Jun 10, 2026

CVE-2026-50564

CVE-2026-50564

Description

Fission versions prior to 1.24.0 allow users to inject dangerous fields into Kubernetes pod specs, leading to potential node compromise and cluster takeover.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Fission versions prior to 1.24.0 allow users to inject dangerous fields into Kubernetes pod specs, leading to potential node compromise and cluster takeover.

Vulnerability

Prior to version 1.24.0, Fission's Environment Custom Resource Definition (CRD) exposed spec.runtime.podSpec and spec.builder.podSpec. These fields were merged into the Kubernetes pod specifications for runtime and builder pods without proper filtering. This allowed users with Environment or Function CRUD permissions to inject dangerous fields such as hostNetwork=true, hostPID=true, privileged=true, hostPath volumes, or a Service Account override into the pods created by Fission's executor and builder service accounts [1, 2]. The Environment.Validate function did not perform adequate security checks on these fields [2].

Exploitation

An attacker with namespace user privileges and create/update permissions on environments.fission.io could create or update an Environment CRD to include malicious fields in the podSpec. If the Kubernetes Pod Security Admission was not configured with enforcing labels (as was the case with the default Fission Helm chart installation), the admission controller would not prevent the creation of these privileged pods [2]. The vulnerability was also bypassable via updates, as the validating webhook was initially configured for create operations only, not update [1].

Impact

By injecting dangerous fields into the pod specs, an attacker could escalate their privileges from a low-privilege tenant operation to a node escape and full cluster takeover. From a privileged, host-networked pod with hostPID, an attacker could use nsenter to access the host's network and filesystem, read cloud-metadata credentials, interact with the container runtime socket, pivot to other namespaces, and potentially compromise the entire node and cluster [2]. The fission-executor and fission-builder service accounts, which create the Deployments, were targeted [1].

Mitigation

This vulnerability was fixed in Fission version 1.24.0, released on 2026-06-10 [3]. The fix involves an admission denylist in pkg/apis/core/v1/podspec_safety.go which rejects dangerous pod-level fields (e.g., HostNetwork, HostPID, ServiceAccountName override, hostPath volumes) and per-container fields (e.g., privileged=true, dangerous capabilities). The validating webhook was also updated to cover create and update operations [1, 2].

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

2
  • Fission/Fissionreferences2 versions
    (expand)+ 1 more
    • (no CPE)
    • (no CPE)range: <1.24.0

Patches

1
e484df8460bb

Reject dangerous PodSpec fields in Environment + Function (GHSA-gx55,… (#3391)

https://github.com/fission/fissionSanket SudakeMay 23, 2026via body-scan-shorthand
10 files changed · +825 21
  • charts/fission-all/templates/webhook-server/webhooks.yaml+1 0 modified
    @@ -72,6 +72,7 @@ webhooks:
         - v1
         operations:
         - CREATE
    +    - UPDATE
         resources:
         - environments
       sideEffects: None
    
  • pkg/apis/core/v1/podspec_safety.go+117 0 added
    @@ -0,0 +1,117 @@
    +/*
    +Copyright 2026 The Fission Authors.
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +    http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +package v1
    +
    +import (
    +	"errors"
    +	"fmt"
    +
    +	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":    {},
    +}
    +
    +// ValidatePodSpecSafety rejects PodSpec fields that would let a low-privilege
    +// tenant escalate to host or cluster level when the executor or buildermgr
    +// schedules a pod from a user-supplied podspec.
    +//
    +// The fission-executor and fission-builder service accounts have the
    +// authority to create Deployments and Pods, so any field that crosses
    +// the container sandbox boundary (host namespaces, privileged contexts,
    +// hostPath mounts, alternate service accounts, dangerous capabilities)
    +// would let a Function- or Environment-CRUD tenant escape the boundary
    +// of their own RBAC and reach node-level state.
    +//
    +// Closes GHSA-gx55-f84r-v3r7, GHSA-wmgg-3p4h-48x7, GHSA-v455-mv2v-5g92.
    +//
    +// The fieldPath argument is used as a prefix in error messages so the
    +// caller can identify which podspec failed (e.g.
    +// "Environment.spec.runtime.podspec" / "Function.spec.podspec").
    +func ValidatePodSpecSafety(fieldPath string, ps *apiv1.PodSpec) error {
    +	if ps == nil {
    +		return nil
    +	}
    +	var errs error
    +
    +	if ps.HostNetwork {
    +		errs = errors.Join(errs, fmt.Errorf("%s.hostNetwork is not allowed", fieldPath))
    +	}
    +	if ps.HostPID {
    +		errs = errors.Join(errs, fmt.Errorf("%s.hostPID is not allowed", fieldPath))
    +	}
    +	if ps.HostIPC {
    +		errs = errors.Join(errs, fmt.Errorf("%s.hostIPC is not allowed", fieldPath))
    +	}
    +	if ps.ServiceAccountName != "" {
    +		errs = errors.Join(errs, fmt.Errorf("%s.serviceAccountName override is not allowed", fieldPath))
    +	}
    +	// DeprecatedServiceAccount is the pre-1.8 alias for ServiceAccountName.
    +	// Kubernetes still honors it for backward compatibility so a tenant could
    +	// otherwise bypass the ServiceAccountName check by setting this field.
    +	if ps.DeprecatedServiceAccount != "" {
    +		errs = errors.Join(errs, fmt.Errorf("%s.serviceAccount (deprecated, alias for serviceAccountName) override is not allowed", fieldPath))
    +	}
    +	for i, v := range ps.Volumes {
    +		if v.HostPath != nil {
    +			errs = errors.Join(errs, fmt.Errorf("%s.volumes[%d].hostPath (%q) is not allowed", fieldPath, i, v.Name))
    +		}
    +	}
    +
    +	checkContainer := func(group string, c apiv1.Container) error {
    +		var cerrs error
    +		sc := c.SecurityContext
    +		if sc == nil {
    +			return nil
    +		}
    +		if sc.Privileged != nil && *sc.Privileged {
    +			cerrs = errors.Join(cerrs, fmt.Errorf(
    +				"%s.%s[%s].securityContext.privileged=true is not allowed", fieldPath, group, c.Name))
    +		}
    +		if sc.AllowPrivilegeEscalation != nil && *sc.AllowPrivilegeEscalation {
    +			cerrs = errors.Join(cerrs, fmt.Errorf(
    +				"%s.%s[%s].securityContext.allowPrivilegeEscalation=true is not allowed", fieldPath, group, c.Name))
    +		}
    +		if sc.Capabilities != nil {
    +			for _, cap := range sc.Capabilities.Add {
    +				if _, bad := dangerousCapabilities[cap]; bad {
    +					cerrs = errors.Join(cerrs, fmt.Errorf(
    +						"%s.%s[%s].securityContext.capabilities.add[%q] is not allowed", fieldPath, group, c.Name, cap))
    +				}
    +			}
    +		}
    +		return cerrs
    +	}
    +
    +	for _, c := range ps.Containers {
    +		errs = errors.Join(errs, checkContainer("containers", c))
    +	}
    +	for _, c := range ps.InitContainers {
    +		errs = errors.Join(errs, checkContainer("initContainers", c))
    +	}
    +
    +	return errs
    +}
    
  • pkg/apis/core/v1/podspec_safety_test.go+202 0 added
    @@ -0,0 +1,202 @@
    +/*
    +Copyright 2026 The Fission Authors.
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +    http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +package v1
    +
    +import (
    +	"strings"
    +	"testing"
    +
    +	apiv1 "k8s.io/api/core/v1"
    +)
    +
    +func TestValidatePodSpecSafety_Nil(t *testing.T) {
    +	if err := ValidatePodSpecSafety("Function.spec.podspec", nil); err != nil {
    +		t.Fatalf("nil podspec must be accepted, got: %v", err)
    +	}
    +}
    +
    +func TestValidatePodSpecSafety_Benign(t *testing.T) {
    +	allow := false
    +	ps := &apiv1.PodSpec{
    +		Containers: []apiv1.Container{{
    +			Name:    "user",
    +			Image:   "alpine:3.19",
    +			Command: []string{"/bin/sh", "-c", "echo hi"},
    +			Env:     []apiv1.EnvVar{{Name: "FOO", Value: "bar"}},
    +			SecurityContext: &apiv1.SecurityContext{
    +				AllowPrivilegeEscalation: &allow,
    +				Capabilities: &apiv1.Capabilities{
    +					Add: []apiv1.Capability{"NET_BIND_SERVICE"},
    +				},
    +			},
    +		}},
    +		Volumes: []apiv1.Volume{{
    +			Name: "cm",
    +			VolumeSource: apiv1.VolumeSource{
    +				ConfigMap: &apiv1.ConfigMapVolumeSource{
    +					LocalObjectReference: apiv1.LocalObjectReference{Name: "my-cm"},
    +				},
    +			},
    +		}},
    +		NodeSelector: map[string]string{"role": "fn"},
    +	}
    +	if err := ValidatePodSpecSafety("Function.spec.podspec", ps); err != nil {
    +		t.Fatalf("benign podspec must be accepted, got: %v", err)
    +	}
    +}
    +
    +func TestValidatePodSpecSafety_DangerousFields(t *testing.T) {
    +	on := true
    +	cases := []struct {
    +		name      string
    +		mutate    func(*apiv1.PodSpec)
    +		wantInErr string
    +	}{
    +		{
    +			name:      "hostNetwork",
    +			mutate:    func(ps *apiv1.PodSpec) { ps.HostNetwork = true },
    +			wantInErr: "hostNetwork",
    +		},
    +		{
    +			name:      "hostPID",
    +			mutate:    func(ps *apiv1.PodSpec) { ps.HostPID = true },
    +			wantInErr: "hostPID",
    +		},
    +		{
    +			name:      "hostIPC",
    +			mutate:    func(ps *apiv1.PodSpec) { ps.HostIPC = true },
    +			wantInErr: "hostIPC",
    +		},
    +		{
    +			name:      "serviceAccountName override",
    +			mutate:    func(ps *apiv1.PodSpec) { ps.ServiceAccountName = "cluster-admin" },
    +			wantInErr: "serviceAccountName",
    +		},
    +		{
    +			name:      "deprecated serviceAccount (alias) override",
    +			mutate:    func(ps *apiv1.PodSpec) { ps.DeprecatedServiceAccount = "cluster-admin" },
    +			wantInErr: "serviceAccount",
    +		},
    +		{
    +			name: "hostPath volume",
    +			mutate: func(ps *apiv1.PodSpec) {
    +				ps.Volumes = []apiv1.Volume{{
    +					Name: "host-root",
    +					VolumeSource: apiv1.VolumeSource{
    +						HostPath: &apiv1.HostPathVolumeSource{Path: "/"},
    +					},
    +				}}
    +			},
    +			wantInErr: "hostPath",
    +		},
    +		{
    +			name: "privileged container",
    +			mutate: func(ps *apiv1.PodSpec) {
    +				ps.Containers = []apiv1.Container{{
    +					Name:            "user",
    +					SecurityContext: &apiv1.SecurityContext{Privileged: &on},
    +				}}
    +			},
    +			wantInErr: "privileged",
    +		},
    +		{
    +			name: "allowPrivilegeEscalation=true",
    +			mutate: func(ps *apiv1.PodSpec) {
    +				ps.Containers = []apiv1.Container{{
    +					Name:            "user",
    +					SecurityContext: &apiv1.SecurityContext{AllowPrivilegeEscalation: &on},
    +				}}
    +			},
    +			wantInErr: "allowPrivilegeEscalation",
    +		},
    +		{
    +			name: "SYS_ADMIN capability",
    +			mutate: func(ps *apiv1.PodSpec) {
    +				ps.Containers = []apiv1.Container{{
    +					Name: "user",
    +					SecurityContext: &apiv1.SecurityContext{
    +						Capabilities: &apiv1.Capabilities{
    +							Add: []apiv1.Capability{"SYS_ADMIN"},
    +						},
    +					},
    +				}}
    +			},
    +			wantInErr: "SYS_ADMIN",
    +		},
    +		{
    +			name: "NET_ADMIN capability",
    +			mutate: func(ps *apiv1.PodSpec) {
    +				ps.Containers = []apiv1.Container{{
    +					Name: "user",
    +					SecurityContext: &apiv1.SecurityContext{
    +						Capabilities: &apiv1.Capabilities{
    +							Add: []apiv1.Capability{"NET_ADMIN"},
    +						},
    +					},
    +				}}
    +			},
    +			wantInErr: "NET_ADMIN",
    +		},
    +		{
    +			name: "privileged init container",
    +			mutate: func(ps *apiv1.PodSpec) {
    +				ps.InitContainers = []apiv1.Container{{
    +					Name:            "init",
    +					SecurityContext: &apiv1.SecurityContext{Privileged: &on},
    +				}}
    +			},
    +			wantInErr: "initContainers",
    +		},
    +	}
    +
    +	for _, tc := range cases {
    +		t.Run(tc.name, func(t *testing.T) {
    +			ps := &apiv1.PodSpec{Containers: []apiv1.Container{{Name: "user"}}}
    +			tc.mutate(ps)
    +			err := ValidatePodSpecSafety("Function.spec.podspec", ps)
    +			if err == nil {
    +				t.Fatalf("expected rejection for %s, got nil", tc.name)
    +			}
    +			if !strings.Contains(err.Error(), tc.wantInErr) {
    +				t.Fatalf("error must mention %q, got: %v", tc.wantInErr, err)
    +			}
    +			if !strings.Contains(err.Error(), "Function.spec.podspec") {
    +				t.Fatalf("error must include the field prefix, got: %v", err)
    +			}
    +		})
    +	}
    +}
    +
    +// 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) {
    +	ps := &apiv1.PodSpec{
    +		Containers: []apiv1.Container{{
    +			Name: "user",
    +			SecurityContext: &apiv1.SecurityContext{
    +				Capabilities: &apiv1.Capabilities{
    +					Add: []apiv1.Capability{"NET_BIND_SERVICE", "CHOWN"},
    +				},
    +			},
    +		}},
    +	}
    +	if err := ValidatePodSpecSafety("Function.spec.podspec", ps); err != nil {
    +		t.Fatalf("non-dangerous capabilities must flow through, got: %v", err)
    +	}
    +}
    
  • pkg/apis/core/v1/validation.go+8 0 modified
    @@ -296,6 +296,9 @@ func (spec FunctionSpec) Validate() error {
     	if spec.InvokeStrategy.ExecutionStrategy.ExecutorType == ExecutorTypeContainer && spec.PodSpec == nil {
     		errs = errors.Join(errs, MakeValidationErr(ErrorInvalidObject, "FunctionSpec.PodSpec", "", "executor type container requires a pod spec"))
     	}
    +	// Reject podspec fields that would let a tenant escalate via the
    +	// executor service account. Closes GHSA-v455-mv2v-5g92.
    +	errs = errors.Join(errs, ValidatePodSpecSafety("Function.spec.podspec", spec.PodSpec))
     
     	// TODO Add below validation warning
     	// if spec.FunctionTimeout <= 0 {
    @@ -666,6 +669,11 @@ func (e *Environment) Validate() error {
     			}
     		}
     	}
    +	// Reject podspec fields that would let a tenant escalate via the
    +	// executor / buildermgr service accounts. Closes GHSA-gx55-f84r-v3r7,
    +	// GHSA-wmgg-3p4h-48x7.
    +	errs = errors.Join(errs, ValidatePodSpecSafety("Environment.spec.runtime.podspec", e.Spec.Runtime.PodSpec))
    +	errs = errors.Join(errs, ValidatePodSpecSafety("Environment.spec.builder.podspec", e.Spec.Builder.PodSpec))
     	return errs
     }
     
    
  • pkg/executor/util/merge.go+120 20 modified
    @@ -79,8 +79,29 @@ func MergePodSpec(srcPodSpec *apiv1.PodSpec, targetPodSpec *apiv1.PodSpec) (*api
     		srcPodSpec.InitContainers = cList
     	}
     
    -	// For volumes - if duplicate exist, throw error
    -	vols, err := mergeVolumeLists(srcPodSpec.Volumes, targetPodSpec.Volumes)
    +	// Sanitize per-container SecurityContext after the merge. The admission
    +	// webhook rejects privileged=true / allowPrivilegeEscalation=true and
    +	// dangerous capabilities (SYS_ADMIN, NET_ADMIN, etc.) at submit time,
    +	// but a webhook-bypass cluster (failurePolicy=Ignore or a stale object
    +	// from a pre-webhook upgrade window) could still reach this code path.
    +	// Strip the dangerous bits from the merged result so the resulting pod
    +	// cannot escape its container even if admission was bypassed. Closes
    +	// GHSA-gx55-f84r-v3r7 / GHSA-wmgg-3p4h-48x7 / GHSA-v455-mv2v-5g92.
    +	for i := range srcPodSpec.Containers {
    +		sanitizeContainerSecurityContext(&srcPodSpec.Containers[i])
    +	}
    +	for i := range srcPodSpec.InitContainers {
    +		sanitizeContainerSecurityContext(&srcPodSpec.InitContainers[i])
    +	}
    +
    +	// For volumes - if duplicate exist, throw error. hostPath volumes are
    +	// stripped from the target before merge: a tenant-supplied hostPath
    +	// mount is a node-escape primitive (read /etc, the container runtime
    +	// socket, etc.). The admission webhook rejects them, and this layer
    +	// makes them unreachable even on webhook-bypass clusters. Closes
    +	// GHSA-gx55-f84r-v3r7 / GHSA-wmgg-3p4h-48x7 / GHSA-v455-mv2v-5g92.
    +	filteredTargetVols := stripHostPathVolumes(targetPodSpec.Volumes)
    +	vols, err := mergeVolumeLists(srcPodSpec.Volumes, filteredTargetVols)
     	if err != nil {
     		multierr = errors.Join(multierr, err)
     	} else {
    @@ -113,6 +134,16 @@ func MergePodSpec(srcPodSpec *apiv1.PodSpec, targetPodSpec *apiv1.PodSpec) (*api
     	}
     
     	// TODO - Security context should be merged instead of overriding.
    +	// Pod-level SecurityContext IS propagated: the chart's
    +	// runtimePodSpec.podSpec.securityContext / builderPodSpec.podSpec.
    +	// securityContext are operator-supplied hardening (fsGroup,
    +	// runAsNonRoot=true, runAsUser=10001, runAsGroup=10001) that must
    +	// reach the pool / builder pods. The node-escape primitives flagged
    +	// by GHSA-gx55-f84r-v3r7 / GHSA-wmgg-3p4h-48x7 / GHSA-v455-mv2v-5g92
    +	// live at container-level (privileged, allowPrivilegeEscalation,
    +	// dangerous capabilities) and at pod level (hostNetwork, hostPID,
    +	// hostIPC, hostPath volumes, serviceAccountName override) — all of
    +	// which are denylisted in pkg/apis/core/v1/podspec_safety.go.
     	if targetPodSpec.SecurityContext != nil {
     		srcPodSpec.SecurityContext = targetPodSpec.SecurityContext
     	}
    @@ -142,29 +173,22 @@ func MergePodSpec(srcPodSpec *apiv1.PodSpec, targetPodSpec *apiv1.PodSpec) (*api
     		srcPodSpec.DNSPolicy = targetPodSpec.DNSPolicy
     	}
     
    -	if targetPodSpec.ServiceAccountName != "" {
    -		srcPodSpec.ServiceAccountName = targetPodSpec.ServiceAccountName
    -	}
    -
    -	if targetPodSpec.DeprecatedServiceAccount != "" {
    -		srcPodSpec.DeprecatedServiceAccount = targetPodSpec.DeprecatedServiceAccount
    -	}
    +	// ServiceAccountName / DeprecatedServiceAccount intentionally not
    +	// propagated: the controller chooses the SA for the pod
    +	// (fission-fetcher for runtime pods, fission-builder for build pods).
    +	// Letting a user-supplied podspec override it would defeat the SA-token
    +	// scoping introduced by GHSA-85g2-pmrx-r49q and GHSA-8wcj-mfrc-jx5q.
    +	// Closes GHSA-gx55-f84r-v3r7 / GHSA-wmgg-3p4h-48x7 / GHSA-v455-mv2v-5g92.
     
     	if targetPodSpec.AutomountServiceAccountToken != nil {
     		srcPodSpec.AutomountServiceAccountToken = targetPodSpec.AutomountServiceAccountToken
     	}
     
    -	if targetPodSpec.HostNetwork {
    -		srcPodSpec.HostNetwork = targetPodSpec.HostNetwork
    -	}
    -
    -	if targetPodSpec.HostPID {
    -		srcPodSpec.HostPID = targetPodSpec.HostPID
    -	}
    -
    -	if targetPodSpec.HostIPC {
    -		srcPodSpec.HostIPC = targetPodSpec.HostIPC
    -	}
    +	// HostNetwork / HostPID / HostIPC intentionally not propagated.
    +	// A pod sharing host namespaces is a node-escape primitive — the
    +	// admission webhook rejects these fields, and this layer makes them
    +	// unreachable even on webhook-bypass clusters.
    +	// Closes GHSA-gx55-f84r-v3r7 / GHSA-wmgg-3p4h-48x7 / GHSA-v455-mv2v-5g92.
     
     	if targetPodSpec.ShareProcessNamespace != nil {
     		srcPodSpec.ShareProcessNamespace = targetPodSpec.ShareProcessNamespace
    @@ -288,3 +312,79 @@ func checkSliceConflicts(field string, objs any) (err error) {
     	}
     	return errs
     }
    +
    +// stripHostPathVolumes returns a copy of vols with any volume whose source
    +// is a hostPath removed. Defense in depth — the admission webhook already
    +// rejects hostPath in tenant-supplied podspecs (see
    +// pkg/apis/core/v1/podspec_safety.go), but on webhook-bypass clusters
    +// (failurePolicy=Ignore, or stale objects from a pre-webhook upgrade
    +// window) this layer makes the dangerous primitive unreachable.
    +// Closes GHSA-gx55-f84r-v3r7 / GHSA-wmgg-3p4h-48x7 / GHSA-v455-mv2v-5g92.
    +func stripHostPathVolumes(vols []apiv1.Volume) []apiv1.Volume {
    +	if len(vols) == 0 {
    +		return vols
    +	}
    +	out := make([]apiv1.Volume, 0, len(vols))
    +	for _, v := range vols {
    +		if v.HostPath != nil {
    +			continue
    +		}
    +		out = append(out, v)
    +	}
    +	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":    {},
    +}
    +
    +// 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.
    +func sanitizeContainerSecurityContext(c *apiv1.Container) {
    +	if c.SecurityContext == nil {
    +		return
    +	}
    +	// Deep-copy before mutating. MergeContainer does a shallow struct copy
    +	// (`dstC := *dst`) and mergo.WithOverride aliases src.SecurityContext
    +	// onto dstC.SecurityContext, so mutating in place would leak into the
    +	// caller's targetPodSpec — which is typically env.Spec.Runtime.PodSpec
    +	// from an informer cache. Allocating a fresh SecurityContext (and a
    +	// fresh Capabilities.Add slice via a new backing array) keeps the
    +	// sanitization local to the merged result.
    +	c.SecurityContext = c.SecurityContext.DeepCopy()
    +	sc := c.SecurityContext
    +	if sc.Privileged != nil && *sc.Privileged {
    +		sc.Privileged = new(false)
    +	}
    +	if sc.AllowPrivilegeEscalation != nil && *sc.AllowPrivilegeEscalation {
    +		sc.AllowPrivilegeEscalation = new(false)
    +	}
    +	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 {
    +				continue
    +			}
    +			filtered = append(filtered, cap)
    +		}
    +		sc.Capabilities.Add = filtered
    +	}
    +}
    
  • pkg/executor/util/merge_test.go+146 0 modified
    @@ -331,3 +331,149 @@ func Test_mergeContainerList(t *testing.T) {
     		})
     	}
     }
    +
    +// TestMergePodSpec_StripsDangerousFields pins the GHSA-gx55 / GHSA-wmgg /
    +// GHSA-v455 invariant on the merge layer: node-escape PodSpec fields supplied
    +// by a target (env.Spec.Runtime.PodSpec / Function.Spec.PodSpec / builder
    +// podSpecPatch) must NOT propagate onto the src spec. The admission webhook
    +// is the primary defence; this is belt-and-braces for clusters running with
    +// failurePolicy=Ignore or stale objects from a pre-webhook upgrade window.
    +//
    +// Pod-level SecurityContext IS propagated (the chart's runtimePodSpec /
    +// builderPodSpec features use it for operator hardening like runAsNonRoot /
    +// fsGroup / runAsUser=10001). The webhook denylist handles tenant-supplied
    +// per-container privileged / allowPrivilegeEscalation / dangerous-cap
    +// vectors which are the actual node-escape primitives.
    +func TestMergePodSpec_StripsDangerousFields(t *testing.T) {
    +	on := true
    +	runAsUser := int64(10001)
    +	runAsNonRoot := true
    +	src := &apiv1.PodSpec{
    +		Containers: []apiv1.Container{{Name: "user", Image: "fission/python-env:latest"}},
    +	}
    +	target := &apiv1.PodSpec{
    +		HostNetwork:        true,
    +		HostPID:            true,
    +		HostIPC:            true,
    +		ServiceAccountName: "cluster-admin",
    +		SecurityContext: &apiv1.PodSecurityContext{
    +			RunAsUser:    &runAsUser,
    +			RunAsNonRoot: &runAsNonRoot,
    +		},
    +		Volumes: []apiv1.Volume{{
    +			Name: "host-root",
    +			VolumeSource: apiv1.VolumeSource{
    +				HostPath: &apiv1.HostPathVolumeSource{Path: "/"},
    +			},
    +		}},
    +		Containers: []apiv1.Container{{
    +			Name:            "user",
    +			SecurityContext: &apiv1.SecurityContext{Privileged: &on},
    +		}},
    +	}
    +
    +	out, _ := MergePodSpec(src, target)
    +
    +	if out.HostNetwork {
    +		t.Errorf("HostNetwork must not propagate from target")
    +	}
    +	if out.HostPID {
    +		t.Errorf("HostPID must not propagate from target")
    +	}
    +	if out.HostIPC {
    +		t.Errorf("HostIPC must not propagate from target")
    +	}
    +	if out.ServiceAccountName != "" {
    +		t.Errorf("ServiceAccountName override must not propagate, got %q", out.ServiceAccountName)
    +	}
    +	// Pod-level SecurityContext MUST flow through to support operator
    +	// hardening from the chart's runtimePodSpec / builderPodSpec features.
    +	if out.SecurityContext == nil {
    +		t.Errorf("pod-level SecurityContext must propagate for operator hardening")
    +	} else if out.SecurityContext.RunAsUser == nil || *out.SecurityContext.RunAsUser != 10001 {
    +		t.Errorf("RunAsUser=10001 must propagate, got %+v", out.SecurityContext.RunAsUser)
    +	}
    +	for _, v := range out.Volumes {
    +		if v.HostPath != nil {
    +			t.Errorf("hostPath volume %q must not propagate", v.Name)
    +		}
    +	}
    +}
    +
    +// 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.
    +func TestMergePodSpec_SanitizesContainerSecurityContext(t *testing.T) {
    +	on := true
    +	src := &apiv1.PodSpec{
    +		Containers: []apiv1.Container{{Name: "user", Image: "fission/python-env:latest"}},
    +	}
    +	target := &apiv1.PodSpec{
    +		Containers: []apiv1.Container{{
    +			Name: "user",
    +			SecurityContext: &apiv1.SecurityContext{
    +				Privileged:               &on,
    +				AllowPrivilegeEscalation: &on,
    +				Capabilities: &apiv1.Capabilities{
    +					Add: []apiv1.Capability{
    +						"SYS_ADMIN",
    +						"NET_BIND_SERVICE", // benign — must flow through
    +						"NET_ADMIN",
    +						"CHOWN", // benign — must flow through
    +					},
    +				},
    +			},
    +		}},
    +		InitContainers: []apiv1.Container{{
    +			Name: "init",
    +			SecurityContext: &apiv1.SecurityContext{
    +				Privileged: &on,
    +			},
    +		}},
    +	}
    +
    +	out, _ := MergePodSpec(src, target)
    +
    +	var merged *apiv1.Container
    +	for i := range out.Containers {
    +		if out.Containers[i].Name == "user" {
    +			merged = &out.Containers[i]
    +			break
    +		}
    +	}
    +	if merged == nil || merged.SecurityContext == nil {
    +		t.Fatalf("merged user container with SecurityContext expected")
    +	}
    +	if merged.SecurityContext.Privileged != nil && *merged.SecurityContext.Privileged {
    +		t.Errorf("Privileged=true must be sanitized to false")
    +	}
    +	if merged.SecurityContext.AllowPrivilegeEscalation != nil && *merged.SecurityContext.AllowPrivilegeEscalation {
    +		t.Errorf("AllowPrivilegeEscalation=true must be sanitized to false")
    +	}
    +	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
    +	}
    +	if !gotBenign["NET_BIND_SERVICE"] {
    +		t.Errorf("benign capability NET_BIND_SERVICE must flow through")
    +	}
    +	if !gotBenign["CHOWN"] {
    +		t.Errorf("benign capability CHOWN must flow through")
    +	}
    +
    +	// InitContainer must also be sanitized.
    +	if out.InitContainers[0].SecurityContext.Privileged != nil && *out.InitContainers[0].SecurityContext.Privileged {
    +		t.Errorf("InitContainer privileged=true must be sanitized to false")
    +	}
    +}
    
  • pkg/webhook/environment.go+5 1 modified
    @@ -42,7 +42,11 @@ func (r *Environment) SetupWebhookWithManager(mgr ctrl.Manager) error {
     var _ webhook.CustomDefaulter = &Environment{}
     
     // user: change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
    -//+kubebuilder:webhook:path=/validate-fission-io-v1-environment,mutating=false,failurePolicy=fail,sideEffects=None,groups=fission.io,resources=environments,verbs=create,versions=v1,name=venvironment.fission.io,admissionReviewVersions=v1
    +// Validation must cover UPDATE as well as CREATE: GHSA-wmgg-3p4h-48x7 noted
    +// that the prior CREATE-only marker let a tenant bypass admission by
    +// posting a clean Environment and then PATCHing in dangerous podspec
    +// fields like hostNetwork or privileged.
    +//+kubebuilder:webhook:path=/validate-fission-io-v1-environment,mutating=false,failurePolicy=fail,sideEffects=None,groups=fission.io,resources=environments,verbs=create;update,versions=v1,name=venvironment.fission.io,admissionReviewVersions=v1
     
     var _ webhook.CustomValidator = &Environment{}
     
    
  • pkg/webhook/environment_test.go+164 0 added
    @@ -0,0 +1,164 @@
    +/*
    +Copyright 2026.
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +    http://www.apache.org/licenses/LICENSE-2.0
    +*/
    +
    +package webhook
    +
    +import (
    +	"strings"
    +	"testing"
    +
    +	apiv1 "k8s.io/api/core/v1"
    +	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    +
    +	v1 "github.com/fission/fission/pkg/apis/core/v1"
    +)
    +
    +func makeValidEnvironment() *v1.Environment {
    +	return &v1.Environment{
    +		ObjectMeta: metav1.ObjectMeta{
    +			Name:      "py",
    +			Namespace: "default",
    +		},
    +		Spec: v1.EnvironmentSpec{
    +			Version: 2,
    +			Runtime: v1.Runtime{
    +				Image: "fission/python-env:latest",
    +			},
    +			Builder: v1.Builder{
    +				Image: "fission/python-builder:latest",
    +			},
    +		},
    +	}
    +}
    +
    +func TestEnvironmentWebhook_Validate_Default(t *testing.T) {
    +	r := &Environment{}
    +	if err := r.Validate(makeValidEnvironment()); err != nil {
    +		t.Fatalf("baseline Environment must validate, got: %v", err)
    +	}
    +}
    +
    +func TestEnvironmentWebhook_Validate_RejectsDangerousPodSpec(t *testing.T) {
    +	on := true
    +	cases := []struct {
    +		name      string
    +		mutate    func(*v1.Environment)
    +		wantInErr string
    +	}{
    +		{
    +			name: "runtime hostNetwork",
    +			mutate: func(e *v1.Environment) {
    +				e.Spec.Runtime.PodSpec = &apiv1.PodSpec{HostNetwork: true}
    +			},
    +			wantInErr: "hostNetwork",
    +		},
    +		{
    +			name: "runtime hostPath volume",
    +			mutate: func(e *v1.Environment) {
    +				e.Spec.Runtime.PodSpec = &apiv1.PodSpec{
    +					Volumes: []apiv1.Volume{{
    +						Name: "host-root",
    +						VolumeSource: apiv1.VolumeSource{
    +							HostPath: &apiv1.HostPathVolumeSource{Path: "/"},
    +						},
    +					}},
    +				}
    +			},
    +			wantInErr: "hostPath",
    +		},
    +		{
    +			name: "runtime privileged container",
    +			mutate: func(e *v1.Environment) {
    +				e.Spec.Runtime.PodSpec = &apiv1.PodSpec{
    +					Containers: []apiv1.Container{{
    +						Name:            "py",
    +						SecurityContext: &apiv1.SecurityContext{Privileged: &on},
    +					}},
    +				}
    +			},
    +			wantInErr: "privileged",
    +		},
    +		{
    +			name: "runtime SYS_ADMIN capability",
    +			mutate: func(e *v1.Environment) {
    +				e.Spec.Runtime.PodSpec = &apiv1.PodSpec{
    +					Containers: []apiv1.Container{{
    +						Name: "py",
    +						SecurityContext: &apiv1.SecurityContext{
    +							Capabilities: &apiv1.Capabilities{
    +								Add: []apiv1.Capability{"SYS_ADMIN"},
    +							},
    +						},
    +					}},
    +				}
    +			},
    +			wantInErr: "SYS_ADMIN",
    +		},
    +		{
    +			name: "builder hostPID",
    +			mutate: func(e *v1.Environment) {
    +				e.Spec.Builder.PodSpec = &apiv1.PodSpec{HostPID: true}
    +			},
    +			wantInErr: "hostPID",
    +		},
    +		{
    +			name: "builder serviceAccountName override",
    +			mutate: func(e *v1.Environment) {
    +				e.Spec.Builder.PodSpec = &apiv1.PodSpec{ServiceAccountName: "cluster-admin"}
    +			},
    +			wantInErr: "serviceAccountName",
    +		},
    +	}
    +
    +	r := &Environment{}
    +	for _, tc := range cases {
    +		t.Run(tc.name, func(t *testing.T) {
    +			env := makeValidEnvironment()
    +			tc.mutate(env)
    +			err := r.Validate(env)
    +			if err == nil {
    +				t.Fatalf("expected rejection for %s, got nil", tc.name)
    +			}
    +			if !strings.Contains(err.Error(), tc.wantInErr) {
    +				t.Fatalf("error must mention %q, got: %v", tc.wantInErr, err)
    +			}
    +		})
    +	}
    +}
    +
    +// TestEnvironmentWebhook_Validate_AcceptsBenignPodSpec ensures the new
    +// safety check doesn't over-reject. Legitimate fields like image,
    +// command, env, configmap volumes, NodeSelector, Tolerations, Resources
    +// must flow through.
    +func TestEnvironmentWebhook_Validate_AcceptsBenignPodSpec(t *testing.T) {
    +	env := makeValidEnvironment()
    +	env.Spec.Runtime.PodSpec = &apiv1.PodSpec{
    +		Containers: []apiv1.Container{{
    +			Name:    "py",
    +			Image:   "fission/python-env:latest",
    +			Command: []string{"/bin/sh", "-c", "echo hi"},
    +			Env:     []apiv1.EnvVar{{Name: "DEBUG", Value: "true"}},
    +		}},
    +		NodeSelector: map[string]string{"role": "fn"},
    +		Tolerations:  []apiv1.Toleration{{Key: "dedicated", Operator: apiv1.TolerationOpEqual, Value: "fn"}},
    +		Volumes: []apiv1.Volume{{
    +			Name: "cm",
    +			VolumeSource: apiv1.VolumeSource{
    +				ConfigMap: &apiv1.ConfigMapVolumeSource{
    +					LocalObjectReference: apiv1.LocalObjectReference{Name: "my-cm"},
    +				},
    +			},
    +		}},
    +	}
    +	r := &Environment{}
    +	if err := r.Validate(env); err != nil {
    +		t.Fatalf("benign Runtime.PodSpec must be accepted, got: %v", err)
    +	}
    +}
    
  • pkg/webhook/function_test.go+61 0 modified
    @@ -14,6 +14,7 @@ import (
     	"strings"
     	"testing"
     
    +	apiv1 "k8s.io/api/core/v1"
     	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
     
     	v1 "github.com/fission/fission/pkg/apis/core/v1"
    @@ -117,3 +118,63 @@ func TestFunctionWebhook_Validate_CrossNamespacePackage(t *testing.T) {
     		})
     	}
     }
    +
    +// TestFunctionWebhook_Validate_RejectsDangerousPodSpec exercises the
    +// container-executor PodSpec safety check. Closes GHSA-v455-mv2v-5g92.
    +func TestFunctionWebhook_Validate_RejectsDangerousPodSpec(t *testing.T) {
    +	on := true
    +	cases := []struct {
    +		name      string
    +		ps        *apiv1.PodSpec
    +		wantInErr string
    +	}{
    +		{
    +			name:      "hostNetwork",
    +			ps:        &apiv1.PodSpec{HostNetwork: true},
    +			wantInErr: "hostNetwork",
    +		},
    +		{
    +			name: "hostPath volume",
    +			ps: &apiv1.PodSpec{
    +				Volumes: []apiv1.Volume{{
    +					Name: "host-root",
    +					VolumeSource: apiv1.VolumeSource{
    +						HostPath: &apiv1.HostPathVolumeSource{Path: "/"},
    +					},
    +				}},
    +			},
    +			wantInErr: "hostPath",
    +		},
    +		{
    +			name: "privileged container",
    +			ps: &apiv1.PodSpec{
    +				Containers: []apiv1.Container{{
    +					Name:            "user",
    +					SecurityContext: &apiv1.SecurityContext{Privileged: &on},
    +				}},
    +			},
    +			wantInErr: "privileged",
    +		},
    +		{
    +			name:      "serviceAccountName override",
    +			ps:        &apiv1.PodSpec{ServiceAccountName: "cluster-admin"},
    +			wantInErr: "serviceAccountName",
    +		},
    +	}
    +
    +	r := &Function{}
    +	for _, tc := range cases {
    +		t.Run(tc.name, func(t *testing.T) {
    +			fn := makeValidFunction("default", "default", "default")
    +			fn.Spec.InvokeStrategy.ExecutionStrategy.ExecutorType = v1.ExecutorTypeContainer
    +			fn.Spec.PodSpec = tc.ps
    +			err := r.Validate(fn)
    +			if err == nil {
    +				t.Fatalf("expected rejection for %s, got nil", tc.name)
    +			}
    +			if !strings.Contains(err.Error(), tc.wantInErr) {
    +				t.Fatalf("error must mention %q, got: %v", tc.wantInErr, err)
    +			}
    +		})
    +	}
    +}
    
  • test/e2e/framework/webhook-manifest.yaml+1 0 modified
    @@ -46,6 +46,7 @@ webhooks:
         - v1
         operations:
         - CREATE
    +    - UPDATE
         resources:
         - environments
       sideEffects: None
    

Vulnerability mechanics

Root cause

"Fission's Environment CRD allowed users to inject dangerous fields into Kubernetes pod specs without proper validation."

Attack vector

A namespace user with create/update permissions on environments.fission.io can create or modify an Environment CRD. By supplying a podspec containing fields like `hostNetwork=true`, `hostPID=true`, `privileged=true`, or a service account override, an attacker can escalate their privileges. This allows them to create privileged pods that can lead to node compromise and potential cluster takeover [ref_id=2]. The issue was exacerbated because the default Fission namespaces lacked Pod Security Admission labels [ref_id=2].

Affected code

The vulnerability lies within Fission's Environment CRD handling, specifically in how `spec.runtime.podSpec` and `spec.builder.podSpec` were merged into Kubernetes pod specs. The `Environment.Validate` function in `pkg/apis/core/v1/validation.go` and the merge logic in `pkg/executor/util/merge.go` are implicated. The admission webhook, implemented in `pkg/webhook/environment.go`, and the validation helper `pkg/apis/core/v1/podspec_safety.go` were modified to address the issue [ref_id=1].

What the fix does

The fix involves two main layers of defense. First, an admission webhook was introduced to validate `Environment.spec.runtime.podSpec` and `spec.builder.podSpec`, rejecting dangerous fields such as `HostNetwork`, `HostPID`, `HostIPC`, `ServiceAccountName` overrides, and `hostPath` volumes [ref_id=1][ref_id=2]. Second, the merge logic in `pkg/executor/util/merge.go` was updated to strip these dangerous fields and sanitize per-container security contexts, providing defense-in-depth against webhook bypass [ref_id=1][ref_id=2]. The webhook's verbs were also updated from `create` to `create;update` to prevent bypass via patching [ref_id=1].

Preconditions

  • authAttacker must have create/update permissions on environments.fission.io.
  • configThe Fission installation must be vulnerable (prior to version 1.24.0).
  • configThe Fission namespaces (e.g., fission-function, fission-builder) must not have Kubernetes Pod Security Admission labels enforcing security policies.

Generated on Jun 10, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

3

News mentions

1