Original flaw
Earlier flawFission pod specifications allowed dangerous Linux capabilities.
Sink: Fission podspec_safety.go dangerousMergeContainerCapabilities
Originally written by Vishal
Loading…
How AI contributed
Incomplete remediationFission v1.24.0 added PodSpec safety validation for tenant-facing Environment and Function CRDs (ValidatePodSpecSafety / ValidateContainerSafety admission webhook + sanitizeContainerSecurityContext executor merge layer), but the capability check was implemented as a fixed denylist of six Linux capabilities (SYS_ADMIN, NET_ADMIN, SYS_PTRACE, SYS_MODULE, DAC_READ_SEARCH, DAC_OVERRIDE). The denylist omitted CAP_SYS_TIME, among others. As a result, a tenant who could create a Function or Environm...
Only the highlighted steps are this advisory. The first card is the earlier flaw the AI tried, and failed, to close.
Original flaw
Earlier flawSink: Fission podspec_safety.go dangerousMergeContainerCapabilities
Originally written by Vishal
This advisoryCVE-2026-50570 (GHSA-QF5V-M7P4-95RP)
AI tried to fix this
The AI change was a real security patch, but it left the same advisory reachable.
Missed: Omitted SYS_TIME from the denylist.
Incomplete AI fix · Claude
Fixed again
This is the patch that actually stops the same attack path.
Code comparison
--- /dev/null+++ b/pkg/apis/core/v1/podspec_safety.go@@ -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+}--- /dev/null+++ b/pkg/apis/core/v1/podspec_safety_test.go@@ -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",--- a/pkg/apis/core/v1/podspec_safety.go+++ b/pkg/apis/core/v1/podspec_safety.go@@ -11,16 +11,20 @@ import ( apiv1 "k8s.io/api/core/v1" ) -// dangerousCapabilities lists Linux capabilities that effectively grant root-// or break the container sandbox. Tenants that can write to Environment or-// Function PodSpec must not be able to add these via securityContext.-var dangerousCapabilities = map[apiv1.Capability]struct{}{- "SYS_ADMIN": {},- "NET_ADMIN": {},- "SYS_PTRACE": {},- "SYS_MODULE": {},- "DAC_READ_SEARCH": {},- "DAC_OVERRIDE": {},+// allowedCapabilities is the strict allowlist of Linux capabilities a tenant+// may request via `securityContext.capabilities.add` on Environment- or+// Function-supplied (init)containers. It matches Kubernetes Pod Security+// Admission's "restricted" profile (only NET_BIND_SERVICE may be added on top+// of the forced drop: ["ALL"] applied at the executor merge layer).+//+// Replaces the previous fixed denylist of six capabilities (SYS_ADMIN,+// NET_ADMIN, SYS_PTRACE, SYS_MODULE, DAC_READ_SEARCH, DAC_OVERRIDE). The+// denylist was structurally incomplete: it omitted at least SYS_TIME (which+// lets a tenant rewrite the shared node wall clock), and could never constrain+// the capabilities the OCI runtime grants by default (the merge layer addresses+// those via drop: ["ALL"]). Closes GHSA-qf5v-m7p4-95rp.+var allowedCapabilities = map[apiv1.Capability]struct{}{+ "NET_BIND_SERVICE": {}, } // ValidatePodSpecSafety rejects PodSpec fields that would let a low-privilege@@ -117,9 +121,10 @@ func ValidateContainerSafety(fieldPath string, c *apiv1.Container) error { } if sc.Capabilities != nil { for _, cap := range sc.Capabilities.Add {- if _, bad := dangerousCapabilities[cap]; bad {+ if _, ok := allowedCapabilities[cap]; !ok { errs = errors.Join(errs, fmt.Errorf(- "%s.securityContext.capabilities.add[%q] is not allowed", fieldPath, cap))+ "%s.securityContext.capabilities.add[%q] is not in the allowlist (only NET_BIND_SERVICE may be added)",+ fieldPath, cap)) } } }Candidate e53d565b2f9de0f0d2313ac5048dc009b4dc3c12080958baf045185e0b0439d2 · Fix 01b0676c09a2a1ad77e366c29bb920434b160f40070ca9ee14625f658c12d67b
Releases
Advisory references