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

CVE-2026-50545

CVE-2026-50545

Description

Fission versions prior to 1.24.0 allow tenants to inject dangerous fields into pod specs, enabling node escape and cluster takeover.

AI Insight

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

Fission versions prior to 1.24.0 allow tenants to inject dangerous fields into pod specs, enabling node escape and cluster takeover.

Vulnerability

Prior to version 1.24.0, Fission, an open-source Kubernetes-native serverless framework, allowed tenants with Environment- or Function-CRUD permissions to inject dangerous fields into pod specifications. This occurred because the Environment.spec.runtime.podSpec and spec.builder.podSpec passthrough lacked validation, and MergePodSpec propagated these fields into generated pods [1, 4]. Specifically, the admission webhook was only configured for create verbs, allowing a patch operation to bypass validation [4].

Exploitation

An attacker with Environment- or Function-CRUD permissions can exploit this vulnerability by first applying a clean Environment and then patching it with dangerous fields such as hostNetwork=true, hostPID=true, privileged=true, hostPath volumes, or a ServiceAccountName override [1, 4]. This allows them to turn low-privilege tenant operations into node-escape and full cluster takeover, leveraging the fission-executor or fission-builder service accounts [1].

Impact

Successful exploitation allows an attacker to achieve node escape and full cluster takeover [1, 4]. By injecting privileged fields, an attacker can gain access to sensitive information like the cluster CA private key, enabling them to sign arbitrary kubelet certificates and compromise the entire cluster [4]. The environments.fission.io create/update RBAC is escalated to these levels [4].

Mitigation

This vulnerability has been fixed in Fission version 1.24.0, released on 2026-05-25 [3]. The fix involves validating pod spec safety by rejecting dangerous fields, extending the webhook marker to verbs=create;update, and stripping dangerous fields at the merge layer [4]. Users are advised to upgrade to version 1.24.0 or later [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

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

Patches

2
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
    
8fa799417c77

Drop fission-builder SA token from user builder container (GHSA-8wcj) (#3390)

https://github.com/fission/fissionSanket SudakeMay 23, 2026via body-scan-shorthand
2 files changed · +275 4
  • pkg/buildermgr/envwatcher.go+47 4 modified
    @@ -439,25 +439,47 @@ func (envw *environmentWatcher) createBuilderDeployment(ctx context.Context, env
     		return nil, err
     	}
     
    +	// AutomountServiceAccountToken=false stops Kubernetes from injecting
    +	// the fission-builder ServiceAccount token into every container in
    +	// the pod. The fetcher sidecar re-mounts the token via the projected
    +	// volume defined below — the user-supplied builder container does
    +	// not. See GHSA-8wcj-mfrc-jx5q (the buildermgr sibling of
    +	// GHSA-85g2-pmrx-r49q).
    +	automountSAToken := false
     	pod := apiv1.PodTemplateSpec{
     		ObjectMeta: metav1.ObjectMeta{
     			Labels:      sel,
     			Annotations: podAnnotations,
     		},
     		Spec: apiv1.PodSpec{
    -			Containers:         []apiv1.Container{*container},
    -			ServiceAccountName: fv1.FissionBuilderSA,
    +			Containers:                   []apiv1.Container{*container},
    +			ServiceAccountName:           fv1.FissionBuilderSA,
    +			AutomountServiceAccountToken: &automountSAToken,
    +			Volumes: []apiv1.Volume{
    +				util.FetcherSATokenProjectedVolume(),
    +			},
     		},
     	}
     
     	if envw.podSpecPatch != nil {
    -
    -		updatedPodSpec, err := util.MergePodSpec(&pod.Spec, envw.podSpecPatch)
    +		// Merge into a deep copy: MergePodSpec mutates its first argument
    +		// in place even on error (it joins partial errors and keeps the
    +		// fields that did merge cleanly). Passing pod.Spec directly would
    +		// leave the deployment with partial patch mutations applied on a
    +		// log-and-continue error path. The copy is discarded on failure
    +		// so pod.Spec retains its pre-merge state.
    +		srcCopy := pod.Spec.DeepCopy()
    +		updatedPodSpec, err := util.MergePodSpec(srcCopy, envw.podSpecPatch)
     		if err == nil {
     			pod.Spec = *updatedPodSpec
     		} else {
     			envw.logger.Error(err, "Failed to merge the specs")
     		}
    +		// Re-clamp after the merge: MergePodSpec propagates a non-nil
    +		// AutomountServiceAccountToken from the patch, which would
    +		// otherwise re-enable the kubelet auto-mount on the user-supplied
    +		// builder container. See GHSA-8wcj-mfrc-jx5q.
    +		pod.Spec.AutomountServiceAccountToken = new(false)
     	}
     
     	pod.Spec = *(util.ApplyImagePullSecret(env.Spec.ImagePullSecret, pod.Spec))
    @@ -500,6 +522,27 @@ func (envw *environmentWatcher) createBuilderDeployment(ctx context.Context, env
     			return nil, err
     		}
     		deployment.Spec.Template.Spec = *newPodSpec
    +		// Re-clamp after the merge: MergePodSpec propagates a non-nil
    +		// AutomountServiceAccountToken from env.Spec.Builder.PodSpec,
    +		// which would otherwise re-enable the kubelet auto-mount on the
    +		// user-supplied builder container. See GHSA-8wcj-mfrc-jx5q.
    +		deployment.Spec.Template.Spec.AutomountServiceAccountToken = new(false)
    +	}
    +
    +	// Re-mount the fission-builder SA token at the canonical Kubernetes
    +	// path on the fetcher container only. The pod-level
    +	// AutomountServiceAccountToken=false flag set above suppresses the
    +	// implicit mount on every container, including fetcher, so we add it
    +	// back explicitly here. This must run AFTER the
    +	// env.Spec.Builder.PodSpec merge — MergePodSpec can append additional
    +	// volumeMounts to the fetcher container, including one at this same
    +	// path, and kubelet would reject the pod with a duplicate-mount-path
    +	// error. The helper strips any pre-existing mount at the path before
    +	// adding its own, so running it last guarantees a single mount on
    +	// the fetcher container backed by the projected SA token volume.
    +	// See GHSA-8wcj-mfrc-jx5q.
    +	if err := util.MountFetcherSATokenOnFetcher(&deployment.Spec.Template.Spec); err != nil {
    +		return nil, err
     	}
     
     	_, err = envw.kubernetesClient.AppsV1().Deployments(ns).Create(ctx, deployment, metav1.CreateOptions{})
    
  • pkg/buildermgr/envwatcher_test.go+228 0 added
    @@ -0,0 +1,228 @@
    +/*
    +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 buildermgr
    +
    +import (
    +	"context"
    +	"testing"
    +
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +	apiv1 "k8s.io/api/core/v1"
    +	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    +	"k8s.io/apimachinery/pkg/types"
    +	k8sfake "k8s.io/client-go/kubernetes/fake"
    +
    +	fv1 "github.com/fission/fission/pkg/apis/core/v1"
    +	"github.com/fission/fission/pkg/executor/util"
    +	fetcherConfig "github.com/fission/fission/pkg/fetcher/config"
    +	"github.com/fission/fission/pkg/utils"
    +	"github.com/fission/fission/pkg/utils/loggerfactory"
    +)
    +
    +// envBuilderContainerName is the name AddFetcherToPodSpec is called with in
    +// envwatcher.createBuilderDeployment for the user-supplied builder container.
    +const envBuilderContainerName = "builder"
    +
    +// newTestEnvironmentWatcher returns an environmentWatcher wired up just enough
    +// to exercise createBuilderDeployment in unit tests. The k8s client is a
    +// fake so Create() is a no-op against in-memory state; createBuilderDeployment
    +// still returns the constructed *appsv1.Deployment which is what the assertions
    +// inspect.
    +func newTestEnvironmentWatcher(t *testing.T) *environmentWatcher {
    +	t.Helper()
    +	cfg, err := fetcherConfig.MakeFetcherConfig("/packages")
    +	require.NoError(t, err)
    +	return &environmentWatcher{
    +		logger:           loggerfactory.GetLogger(),
    +		kubernetesClient: k8sfake.NewSimpleClientset(),
    +		nsResolver:       utils.DefaultNSResolver(),
    +		fetcherConfig:    cfg,
    +		cache:            map[types.UID]*builderInfo{},
    +	}
    +}
    +
    +func newTestBuilderEnv() *fv1.Environment {
    +	return &fv1.Environment{
    +		ObjectMeta: metav1.ObjectMeta{
    +			Name:      "py",
    +			Namespace: "default",
    +		},
    +		Spec: fv1.EnvironmentSpec{
    +			Version: 2,
    +			Runtime: fv1.Runtime{
    +				Image: "fission/python-env:latest",
    +			},
    +			Builder: fv1.Builder{
    +				Image: "fission/python-builder:latest",
    +			},
    +		},
    +	}
    +}
    +
    +// TestBuilderPodSpecDoesNotAutomountTokenInBuilderContainer pins the
    +// security-advisory-8wcj invariant: the fission-builder SA token is only
    +// mounted inside the fetcher sidecar, never in the user-supplied builder
    +// container. See GHSA-8wcj-mfrc-jx5q.
    +func TestBuilderPodSpecDoesNotAutomountTokenInBuilderContainer(t *testing.T) {
    +	envw := newTestEnvironmentWatcher(t)
    +	env := newTestBuilderEnv()
    +
    +	deployment, err := envw.createBuilderDeployment(context.Background(), env, "default")
    +	require.NoError(t, err)
    +	pod := deployment.Spec.Template
    +
    +	// Pod-level AutomountServiceAccountToken must be explicitly false.
    +	require.NotNil(t, pod.Spec.AutomountServiceAccountToken,
    +		"pod-level AutomountServiceAccountToken must be set, not nil")
    +	assert.False(t, *pod.Spec.AutomountServiceAccountToken,
    +		"pod-level AutomountServiceAccountToken must be false")
    +
    +	// Pod still runs as fission-builder so the fetcher container can use
    +	// its projected token to talk to the API server.
    +	assert.Equal(t, fv1.FissionBuilderSA, pod.Spec.ServiceAccountName)
    +
    +	// Projected SA-token volume must exist.
    +	var projected *apiv1.Volume
    +	for i := range pod.Spec.Volumes {
    +		if pod.Spec.Volumes[i].Name == util.FetcherSATokenVolumeName {
    +			projected = &pod.Spec.Volumes[i]
    +			break
    +		}
    +	}
    +	require.NotNil(t, projected, "projected SA token volume %q must exist", util.FetcherSATokenVolumeName)
    +	require.NotNil(t, projected.Projected, "%q must be a projected volume", util.FetcherSATokenVolumeName)
    +
    +	// Locate fetcher + builder containers.
    +	var fetcher, builder *apiv1.Container
    +	for i := range pod.Spec.Containers {
    +		switch pod.Spec.Containers[i].Name {
    +		case util.FetcherContainerName:
    +			fetcher = &pod.Spec.Containers[i]
    +		case envBuilderContainerName:
    +			builder = &pod.Spec.Containers[i]
    +		}
    +	}
    +	require.NotNil(t, fetcher, "fetcher container must be present")
    +	require.NotNil(t, builder, "builder container must be present")
    +
    +	// Fetcher must mount the projected SA token at the canonical k8s path.
    +	fetcherHasMount := false
    +	for _, vm := range fetcher.VolumeMounts {
    +		if vm.MountPath == util.FetcherSATokenMountPath {
    +			fetcherHasMount = true
    +			assert.Equal(t, util.FetcherSATokenVolumeName, vm.Name,
    +				"fetcher SA-token mount must be backed by the projected volume")
    +			assert.True(t, vm.ReadOnly, "fetcher SA-token mount must be read-only")
    +		}
    +	}
    +	assert.True(t, fetcherHasMount, "fetcher must mount its own SA token")
    +
    +	// Builder (user) container must have NO mount at the SA-token path.
    +	for _, vm := range builder.VolumeMounts {
    +		assert.NotEqual(t, util.FetcherSATokenMountPath, vm.MountPath,
    +			"builder container must not have any mount at the SA token path")
    +	}
    +}
    +
    +// TestBuilderPodSpecPatchCannotReEnableAutomount asserts that an envw with a
    +// podSpecPatch that sets AutomountServiceAccountToken=true cannot override the
    +// invariant. The re-clamp after MergePodSpec is what blocks this. See
    +// GHSA-8wcj-mfrc-jx5q.
    +func TestBuilderPodSpecPatchCannotReEnableAutomount(t *testing.T) {
    +	envw := newTestEnvironmentWatcher(t)
    +	envw.podSpecPatch = &apiv1.PodSpec{
    +		AutomountServiceAccountToken: new(true),
    +	}
    +	env := newTestBuilderEnv()
    +
    +	deployment, err := envw.createBuilderDeployment(context.Background(), env, "default")
    +	require.NoError(t, err)
    +	pod := deployment.Spec.Template
    +
    +	require.NotNil(t, pod.Spec.AutomountServiceAccountToken)
    +	assert.False(t, *pod.Spec.AutomountServiceAccountToken,
    +		"envw.podSpecPatch must not be able to re-enable auto-mount")
    +}
    +
    +// TestBuilderEnvBuilderPodSpecCannotReEnableAutomount asserts that an
    +// env.Spec.Builder.PodSpec with AutomountServiceAccountToken=true cannot
    +// override the invariant.
    +func TestBuilderEnvBuilderPodSpecCannotReEnableAutomount(t *testing.T) {
    +	envw := newTestEnvironmentWatcher(t)
    +	env := newTestBuilderEnv()
    +	env.Spec.Builder.PodSpec = &apiv1.PodSpec{
    +		AutomountServiceAccountToken: new(true),
    +	}
    +
    +	deployment, err := envw.createBuilderDeployment(context.Background(), env, "default")
    +	require.NoError(t, err)
    +	pod := deployment.Spec.Template
    +
    +	require.NotNil(t, pod.Spec.AutomountServiceAccountToken)
    +	assert.False(t, *pod.Spec.AutomountServiceAccountToken,
    +		"env.Spec.Builder.PodSpec must not be able to re-enable auto-mount")
    +}
    +
    +// TestBuilderEnvBuilderPodSpecCannotIntroduceDuplicateSATokenMount pins the
    +// invariant from PR #3366 (Copilot Round-3) for the buildermgr path: an env
    +// author who supplies env.Spec.Builder.PodSpec.Containers = [{name: "fetcher",
    +// volumeMounts: [{mountPath: <SA token path>}]}] must not cause the final
    +// fetcher container to end up with two mounts at the SA-token path.
    +func TestBuilderEnvBuilderPodSpecCannotIntroduceDuplicateSATokenMount(t *testing.T) {
    +	envw := newTestEnvironmentWatcher(t)
    +	env := newTestBuilderEnv()
    +	env.Spec.Builder.PodSpec = &apiv1.PodSpec{
    +		Containers: []apiv1.Container{
    +			{
    +				Name: util.FetcherContainerName,
    +				VolumeMounts: []apiv1.VolumeMount{
    +					{
    +						Name:      "evil-sa-mount",
    +						MountPath: util.FetcherSATokenMountPath,
    +					},
    +				},
    +			},
    +		},
    +	}
    +
    +	deployment, err := envw.createBuilderDeployment(context.Background(), env, "default")
    +	require.NoError(t, err)
    +	pod := deployment.Spec.Template
    +
    +	var fetcher *apiv1.Container
    +	for i := range pod.Spec.Containers {
    +		if pod.Spec.Containers[i].Name == util.FetcherContainerName {
    +			fetcher = &pod.Spec.Containers[i]
    +			break
    +		}
    +	}
    +	require.NotNil(t, fetcher, "fetcher container must be present")
    +
    +	mountsAtSAPath := 0
    +	var mountVolumeName string
    +	for _, vm := range fetcher.VolumeMounts {
    +		if vm.MountPath == util.FetcherSATokenMountPath {
    +			mountsAtSAPath++
    +			mountVolumeName = vm.Name
    +		}
    +	}
    +	assert.Equal(t, 1, mountsAtSAPath,
    +		"fetcher must have exactly one mount at the SA-token path, not duplicates")
    +	assert.Equal(t, util.FetcherSATokenVolumeName, mountVolumeName,
    +		"the sole mount at the SA-token path must be the projected volume, not the user-supplied one")
    +}
    

Vulnerability mechanics

Root cause

"The Environment.spec.runtime.podSpec and spec.builder.podSpec passthrough lacked validation, allowing dangerous fields to be propagated into generated pods."

Attack vector

An attacker with Environment- or Function-CRUD permissions can supply a podspec containing dangerous fields such as `hostNetwork=true`, `hostPID=true`, `privileged=true`, `hostPath` volumes, or a ServiceAccount override. This allows a low-privilege tenant to perform node-escape and gain full cluster takeover via the fission-executor or fission-builder service accounts. The vulnerability could be bypassed by patching a clean Environment object with dangerous fields after its creation, as the webhook initially only validated `verbs=create` [ref_id=1].

Affected code

The vulnerability lies in the passthrough of `Environment.spec.runtime.podSpec` and `spec.builder.podSpec`, and the `MergePodSpec` function which propagated dangerous fields. Specifically, the validation logic in `pkg/apis/core/v1/validation.go` and the webhook logic in `pkg/webhook/environment.go` were insufficient. The `pkg/executor/util/merge.go` file also contained logic that unconditionally propagated certain fields [ref_id=1].

What the fix does

The patch introduces validation for dangerous fields in `Environment.spec.runtime.podSpec` and `spec.builder.podSpec`, as well as `Function.spec.podspec` [ref_id=1]. A new function `ValidatePodSpecSafety` in `pkg/apis/core/v1/podspec_safety.go` rejects fields like `HostNetwork`, `HostPID`, `HostIPC`, `ServiceAccountName` override, `hostPath` volumes, and per-container privileged settings. Additionally, the `MergePodSpec` logic in `pkg/executor/util/merge.go` was updated to strip dangerous fields before merging, providing defense-in-depth against webhook bypasses [ref_id=1]. The validating webhook was also updated to intercept `verbs=create,update` to prevent update bypasses [ref_id=1].

Preconditions

  • authAttacker must have Environment- or Function-CRUD permissions.

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

References

4

News mentions

1