CVE-2026-61549: Woodpecker CI Kubernetes Backend Privilege Escalation via serviceAccountName
Any user with push access to a repository connected to Woodpecker's Kubernetes backend could set an arbitrary ServiceAccount on their pipeline pods, inheriting that account's full cluster RBAC…
The problem
Woodpecker's Kubernetes backend reads the backend_options.kubernetes.serviceAccountName field from a pipeline YAML file and passes it directly to the pod spec. There was no agent-side gate, so any repository contributor with push access could specify any ServiceAccount in the pipeline namespace.
If a privileged ServiceAccount existed in that namespace, the attacker's pipeline pod would receive its token at /var/run/secrets/kubernetes.io/serviceaccount/token. That token could then be used to read Kubernetes secrets, enumerate cluster resources, or escalate to cluster-admin.
Proof of concept
A working proof-of-concept for CVE-2026-61549 in go.woodpecker-ci.org/woodpecker/v3, with the exact payload below.
# .woodpecker.yml - push this to any repo connected to a vulnerable Woodpecker instance
steps:
- name: steal-token
image: alpine
commands:
- cat /var/run/secrets/kubernetes.io/serviceaccount/token
- env
backend_options:
kubernetes:
serviceAccountName: cluster-admin-sa # any privileged SA in the pipeline namespaceBefore v3.16.0, podSpec() in pipeline/backend/kubernetes/pod.go assigned options.ServiceAccountName to the pod spec unconditionally, with no check against the agent configuration. The fix in PR #6792 added a boolean gate (ServiceAccountNameAllowFromStep) to the agent config struct; the field is only forwarded to the pod spec when that flag is explicitly enabled by the operator.
The new default is deny. The controlling env var is WOODPECKER_BACKEND_K8S_SERVICE_ACCOUNT_NAME_ALLOW_FROM_STEP, which defaults to false. This mirrors the existing pattern already used for pod labels and annotations. CWE-269 (Improper Privilege Management) and CWE-862 (Missing Authorization) both apply: user-supplied data elevated cluster permissions with zero authorization check.
The fix
Upgrade to Woodpecker v3.16.0. After upgrading, serviceAccountName in step backend options is silently ignored unless the agent is explicitly started with WOODPECKER_BACKEND_K8S_SERVICE_ACCOUNT_NAME_ALLOW_FROM_STEP=true. Only set that flag if every user with push access to connected repositories is trusted.
As an immediate workaround on older versions: ensure no privileged ServiceAccount exists in the pipeline namespace, or enforce an admission policy (Kyverno, OPA/Gatekeeper, ValidatingAdmissionPolicy) that rejects pods with unexpected serviceAccountName values.
Related research
- high · 7.5CVE-2026-54719CVE-2026-54719: goshs .goshs ACL Bypass via ?bulk Zip-Download Route
- high · 8.5CVE-2026-50570CVE-2026-50570: Fission Incomplete Capability Denylist Allows CAP_SYS_TIME Privilege Escalation
- high · 8.2CVE-2026-27771CVE-2026-27771: Gitea Container Registry Authentication Bypass
- high · 7.7nebula-mesh: Non-Admin SSRF via Unguarded allow_private Webhook Flag