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

CVE-2026-50566

CVE-2026-50566

Description

Fission before 1.24.0 allows privileged containers via RBAC, leading to sandbox escape and potential cluster compromise.

AI Insight

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

Fission before 1.24.0 allows privileged containers via RBAC, leading to sandbox escape and potential cluster compromise.

Vulnerability

Prior to version 1.24.0, Fission, an open-source Kubernetes-native serverless framework, allowed a tenant with environments.fission.io create/update RBAC privileges to deploy containers with elevated privileges, such as privileged: true or allowPrivilegeEscalation: true, within Fission's function or builder namespaces. This was due to an admission-layer gap where the Environment.Validate() function only inspected *PodSpec and not standalone Container.SecurityContext fields, and a merge-layer gap where sanitizeContainerSecurityContext() was not consistently applied during container merging [1], [3].

Exploitation

An attacker with environments.fission.io create/update RBAC permissions can create a Fission Environment resource specifying a spec.runtime.container with a securityContext that includes privileged: true, allowPrivilegeEscalation: true, or dangerous capabilities like SYS_ADMIN. The Fission admission webhook would accept this configuration, and the resulting pool pod would be scheduled using the executor's high-privilege service account, enabling the exploitation [1].

Impact

Successful exploitation allows an attacker to achieve container sandbox escape, gain access to the host filesystem and network, and potentially compromise the node and the entire Kubernetes cluster. This is facilitated by the attacker running privileged containers under the executor's high-privilege service account [1].

Mitigation

This vulnerability has been patched in Fission version 1.24.0, released on 2026-05-25 [2]. Users are advised to upgrade to version 1.24.0 or later to remediate this issue. No workarounds are specified in the available references.

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
695d3e97e3a2

fix(security): validate Runtime/Builder Container SecurityContext (#3406)

https://github.com/fission/fissionSanket SudakeMay 26, 2026via body-scan-shorthand
6 files changed · +217 28
  • pkg/apis/core/v1/podspec_safety.go+50 28 modified
    @@ -69,37 +69,59 @@ func ValidatePodSpecSafety(fieldPath string, ps *apiv1.PodSpec) error {
     		}
     	}
     
    -	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 i := range ps.Containers {
    +		group := fmt.Sprintf("%s.containers[%s]", fieldPath, ps.Containers[i].Name)
    +		errs = errors.Join(errs, ValidateContainerSafety(group, &ps.Containers[i]))
    +	}
    +	for i := range ps.InitContainers {
    +		group := fmt.Sprintf("%s.initContainers[%s]", fieldPath, ps.InitContainers[i].Name)
    +		errs = errors.Join(errs, ValidateContainerSafety(group, &ps.InitContainers[i]))
     	}
     
    -	for _, c := range ps.Containers {
    -		errs = errors.Join(errs, checkContainer("containers", c))
    +	return errs
    +}
    +
    +// ValidateContainerSafety rejects the SecurityContext fields of a single
    +// container that would let a low-privilege tenant escape the container
    +// sandbox: privileged=true, allowPrivilegeEscalation=true, and dangerous
    +// Linux capabilities.
    +//
    +// It exists as a standalone check because the Environment CRD exposes
    +// `spec.runtime.container` and `spec.builder.container` — a bare
    +// *apiv1.Container that is merged into the runtime/builder pod but is
    +// NOT part of any PodSpec, so ValidatePodSpecSafety never reaches it.
    +// Leaving the Container SecurityContext unchecked is a bypass of the
    +// PodSpec hardening (GHSA-gx55-f84r-v3r7 / GHSA-wmgg-3p4h-48x7 /
    +// GHSA-v455-mv2v-5g92) — closes GHSA-m63v-2g9w-2w6v.
    +//
    +// ValidatePodSpecSafety calls this for each (init)container, and
    +// Environment.Validate calls it directly for Runtime.Container /
    +// Builder.Container. A nil container is accepted (the field is optional).
    +//
    +// The fieldPath argument is used as a prefix in error messages so the
    +// caller can identify which container failed (e.g.
    +// "Environment.spec.runtime.container").
    +func ValidateContainerSafety(fieldPath string, c *apiv1.Container) error {
    +	if c == nil || c.SecurityContext == nil {
    +		return nil
    +	}
    +	var errs error
    +	sc := c.SecurityContext
    +	if sc.Privileged != nil && *sc.Privileged {
    +		errs = errors.Join(errs, fmt.Errorf(
    +			"%s.securityContext.privileged=true is not allowed", fieldPath))
     	}
    -	for _, c := range ps.InitContainers {
    -		errs = errors.Join(errs, checkContainer("initContainers", c))
    +	if sc.AllowPrivilegeEscalation != nil && *sc.AllowPrivilegeEscalation {
    +		errs = errors.Join(errs, fmt.Errorf(
    +			"%s.securityContext.allowPrivilegeEscalation=true is not allowed", fieldPath))
    +	}
    +	if sc.Capabilities != nil {
    +		for _, cap := range sc.Capabilities.Add {
    +			if _, bad := dangerousCapabilities[cap]; bad {
    +				errs = errors.Join(errs, fmt.Errorf(
    +					"%s.securityContext.capabilities.add[%q] is not allowed", fieldPath, cap))
    +			}
    +		}
     	}
    -
     	return errs
     }
    
  • pkg/apis/core/v1/podspec_safety_test.go+59 0 modified
    @@ -169,6 +169,65 @@ func TestValidatePodSpecSafety_DangerousFields(t *testing.T) {
     	}
     }
     
    +// TestValidateContainerSafety covers the standalone-container check used for
    +// Environment Runtime.Container / Builder.Container. Closes GHSA-m63v-2g9w-2w6v.
    +func TestValidateContainerSafety(t *testing.T) {
    +	on := true
    +	off := false
    +
    +	t.Run("nil container is accepted", func(t *testing.T) {
    +		if err := ValidateContainerSafety("Environment.spec.runtime.container", nil); err != nil {
    +			t.Fatalf("nil container must be accepted, got: %v", err)
    +		}
    +	})
    +
    +	t.Run("nil securityContext is accepted", func(t *testing.T) {
    +		c := &apiv1.Container{Name: "py", Image: "fission/python-env:latest"}
    +		if err := ValidateContainerSafety("Environment.spec.runtime.container", c); err != nil {
    +			t.Fatalf("container without securityContext must be accepted, got: %v", err)
    +		}
    +	})
    +
    +	t.Run("benign securityContext is accepted", func(t *testing.T) {
    +		c := &apiv1.Container{
    +			Name: "py",
    +			SecurityContext: &apiv1.SecurityContext{
    +				AllowPrivilegeEscalation: &off,
    +				Capabilities:             &apiv1.Capabilities{Add: []apiv1.Capability{"NET_BIND_SERVICE"}},
    +			},
    +		}
    +		if err := ValidateContainerSafety("Environment.spec.runtime.container", c); err != nil {
    +			t.Fatalf("benign container must be accepted, got: %v", err)
    +		}
    +	})
    +
    +	cases := []struct {
    +		name      string
    +		sc        *apiv1.SecurityContext
    +		wantInErr string
    +	}{
    +		{"privileged", &apiv1.SecurityContext{Privileged: &on}, "privileged"},
    +		{"allowPrivilegeEscalation", &apiv1.SecurityContext{AllowPrivilegeEscalation: &on}, "allowPrivilegeEscalation"},
    +		{"SYS_ADMIN", &apiv1.SecurityContext{Capabilities: &apiv1.Capabilities{Add: []apiv1.Capability{"SYS_ADMIN"}}}, "SYS_ADMIN"},
    +		{"NET_ADMIN", &apiv1.SecurityContext{Capabilities: &apiv1.Capabilities{Add: []apiv1.Capability{"NET_ADMIN"}}}, "NET_ADMIN"},
    +	}
    +	for _, tc := range cases {
    +		t.Run(tc.name, func(t *testing.T) {
    +			c := &apiv1.Container{Name: "py", SecurityContext: tc.sc}
    +			err := ValidateContainerSafety("Environment.spec.runtime.container", c)
    +			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(), "Environment.spec.runtime.container") {
    +				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
    
  • pkg/apis/core/v1/validation.go+8 0 modified
    @@ -662,6 +662,14 @@ func (e *Environment) Validate() error {
     	// 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))
    +	// The standalone Runtime.Container / Builder.Container fields are merged
    +	// into the runtime / builder pod without going through any PodSpec, so
    +	// ValidatePodSpecSafety above does not reach them. Validate their
    +	// SecurityContext directly — otherwise a tenant could set
    +	// spec.runtime.container.securityContext.privileged=true and bypass the
    +	// PodSpec hardening. Closes GHSA-m63v-2g9w-2w6v.
    +	errs = errors.Join(errs, ValidateContainerSafety("Environment.spec.runtime.container", e.Spec.Runtime.Container))
    +	errs = errors.Join(errs, ValidateContainerSafety("Environment.spec.builder.container", e.Spec.Builder.Container))
     	return errs
     }
     
    
  • pkg/executor/util/merge.go+11 0 modified
    @@ -39,6 +39,17 @@ func MergeContainer(dst *apiv1.Container, src *apiv1.Container) (*apiv1.Containe
     		checkSliceConflicts("Name", dstC.VolumeMounts),
     		checkSliceConflicts("Name", dstC.VolumeDevices))
     
    +	// mergo.WithOverride copies src.SecurityContext onto the merged result,
    +	// so a tenant-supplied Environment Runtime.Container / Builder.Container
    +	// with privileged=true / allowPrivilegeEscalation=true / dangerous caps
    +	// would otherwise reach the running pod. The admission webhook
    +	// (ValidateContainerSafety) is the primary defence; this strips the
    +	// dangerous bits at the merge layer so a webhook-bypass cluster
    +	// (failurePolicy=Ignore or stale objects from a pre-webhook upgrade)
    +	// is still protected. Closes GHSA-m63v-2g9w-2w6v (the Container-field
    +	// sibling of GHSA-gx55 / GHSA-wmgg / GHSA-v455).
    +	sanitizeContainerSecurityContext(&dstC)
    +
     	return &dstC, errs
     }
     
    
  • pkg/executor/util/merge_test.go+55 0 modified
    @@ -156,6 +156,61 @@ func Test_mergeContainer(t *testing.T) {
     	}
     }
     
    +// TestMergeContainer_SanitizesSecurityContext pins the GHSA-m63v-2g9w-2w6v
    +// invariant: MergeContainer is the path for the Environment Runtime.Container /
    +// Builder.Container fields, which do not go through any PodSpec and so are not
    +// reached by MergePodSpec's sanitizer. A tenant-supplied container with
    +// privileged=true / allowPrivilegeEscalation=true / dangerous caps must be
    +// sanitized in the merged result, while the caller's source container must not
    +// be mutated (it is typically env.Spec.Runtime.Container from an informer
    +// cache).
    +func TestMergeContainer_SanitizesSecurityContext(t *testing.T) {
    +	on := true
    +	dst := &apiv1.Container{Name: "py", Image: "fission/python-env:latest"}
    +	src := &apiv1.Container{
    +		Name: "py",
    +		SecurityContext: &apiv1.SecurityContext{
    +			Privileged:               &on,
    +			AllowPrivilegeEscalation: &on,
    +			Capabilities: &apiv1.Capabilities{
    +				Add: []apiv1.Capability{"SYS_ADMIN", "NET_BIND_SERVICE", "NET_ADMIN"},
    +			},
    +		},
    +	}
    +
    +	out, err := MergeContainer(dst, src)
    +	if err != nil {
    +		t.Fatalf("MergeContainer error: %v", err)
    +	}
    +	if out.SecurityContext == nil {
    +		t.Fatalf("merged container must keep a SecurityContext")
    +	}
    +	if out.SecurityContext.Privileged != nil && *out.SecurityContext.Privileged {
    +		t.Errorf("Privileged=true must be sanitized to false")
    +	}
    +	if out.SecurityContext.AllowPrivilegeEscalation != nil && *out.SecurityContext.AllowPrivilegeEscalation {
    +		t.Errorf("AllowPrivilegeEscalation=true must be sanitized to false")
    +	}
    +	gotCaps := map[apiv1.Capability]bool{}
    +	for _, c := range out.SecurityContext.Capabilities.Add {
    +		gotCaps[c] = true
    +	}
    +	if gotCaps["SYS_ADMIN"] || gotCaps["NET_ADMIN"] {
    +		t.Errorf("dangerous capabilities must be stripped, got %v", out.SecurityContext.Capabilities.Add)
    +	}
    +	if !gotCaps["NET_BIND_SERVICE"] {
    +		t.Errorf("benign capability NET_BIND_SERVICE must flow through")
    +	}
    +
    +	// The caller's source container must not be mutated by the merge.
    +	if src.SecurityContext.Privileged == nil || !*src.SecurityContext.Privileged {
    +		t.Errorf("source container Privileged must be left untouched (deep copy expected)")
    +	}
    +	if len(src.SecurityContext.Capabilities.Add) != 3 {
    +		t.Errorf("source container capabilities must be left untouched, got %v", src.SecurityContext.Capabilities.Add)
    +	}
    +}
    +
     func Test_mergeVolumeLists(t *testing.T) {
     	type args struct {
     		dst []apiv1.Volume
    
  • pkg/webhook/environment_test.go+34 0 modified
    @@ -109,6 +109,40 @@ func TestEnvironmentWebhook_Validate_RejectsDangerousPodSpec(t *testing.T) {
     			},
     			wantInErr: "serviceAccountName",
     		},
    +		// Runtime.Container / Builder.Container are a separate injection path
    +		// from the PodSpec cases above. Closes GHSA-m63v-2g9w-2w6v.
    +		{
    +			name: "runtime container privileged",
    +			mutate: func(e *v1.Environment) {
    +				e.Spec.Runtime.Container = &apiv1.Container{
    +					Name:            "py",
    +					SecurityContext: &apiv1.SecurityContext{Privileged: &on},
    +				}
    +			},
    +			wantInErr: "privileged",
    +		},
    +		{
    +			name: "runtime container SYS_ADMIN capability",
    +			mutate: func(e *v1.Environment) {
    +				e.Spec.Runtime.Container = &apiv1.Container{
    +					Name: "py",
    +					SecurityContext: &apiv1.SecurityContext{
    +						Capabilities: &apiv1.Capabilities{Add: []apiv1.Capability{"SYS_ADMIN"}},
    +					},
    +				}
    +			},
    +			wantInErr: "SYS_ADMIN",
    +		},
    +		{
    +			name: "builder container allowPrivilegeEscalation",
    +			mutate: func(e *v1.Environment) {
    +				e.Spec.Builder.Container = &apiv1.Container{
    +					Name:            "py-builder",
    +					SecurityContext: &apiv1.SecurityContext{AllowPrivilegeEscalation: &on},
    +				}
    +			},
    +			wantInErr: "allowPrivilegeEscalation",
    +		},
     	}
     
     	r := &Environment{}
    
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

"The Fission Environment CRD did not properly validate the SecurityContext of standalone containers, bypassing existing PodSpec hardening measures."

Attack vector

An attacker with `environments.fission.io` create/update RBAC can create a malicious Environment resource. This resource specifies a standalone container within the `spec.runtime.container` or `spec.builder.container` fields with elevated privileges, such as `privileged: true` or dangerous capabilities. The Fission admission controller fails to validate this standalone container's SecurityContext, allowing the creation of a pod with these dangerous settings. This pod is then scheduled using the executor's high-privilege service account, enabling further exploitation.

Affected code

The vulnerability exists in the Fission admission layer and merge layer. Specifically, `Environment.Validate()` did not inspect `Runtime.Container.SecurityContext` or `Builder.Container.SecurityContext` because `ValidatePodSpecSafety()` only operates on `*PodSpec`. Furthermore, `sanitizeContainerSecurityContext()` was not invoked within `MergeContainer()`, leading to a bypass when only the `container` field was set and `Runtime.PodSpec` was nil. Affected merge sites include `poolmgr` (gp_deployment.go), `newdeploy` (newdeploy.go), and `buildermgr` (envwatcher.go).

What the fix does

The fix introduces a new validation function, `ValidateContainerSafety`, which is called by `Environment.Validate()` for standalone `Runtime.Container` and `Builder.Container` fields. This function enforces the denylist for privileged settings and dangerous capabilities directly on these standalone containers. Additionally, `sanitizeContainerSecurityContext()` is now invoked within `MergeContainer()` to ensure security context sanitization occurs during container merging, providing defense in depth.

Preconditions

  • authThe attacker must have `environments.fission.io` create/update RBAC permissions.

Reproduction

```yaml apiVersion: fission.io/v1 kind: Environment metadata: name: priv-escape-test namespace: default spec: version: 3 runtime: image: "ghcr.io/fission/python-env:latest" container: name: priv-escape-test securityContext: privileged: true poolsize: 1 ``` The admission webhook accepts this Environment, and the resulting pool pod runs with `privileged: true`.

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