NewN-Day-BenchView benchmark
winfunc
Back to Hacktivity

Status: Patched

This vulnerability has been verified as resolved and deployed.

ZenML logo
ZenMLHigh2026-08-28

Workload API tokens can be exchanged for persistent unscoped user tokens

Summary

The generic-token branch discarded workload claims before the parent token expired

Code running inside a ZenML pipeline workload could exchange its run-bound token for a generic user token that remained valid after the workload ended, preserving the issuing principal's control-plane RBAC authority for up to the generic-token lifetime.

Root cause. The api_token() generic branch returned before the confinement logic used for workload-token issuance. It called generate_access_token() with only user_id and expires_in, silently dropping schedule_id, pipeline_run_id, and deployment_id claims from the authenticated parent.

Remediation evidence. ZenML opened PR #5152 five days after the disclosure and merged it on 28 August 2026. The PR is an exact remediation: it prevents a scoped workload credential from being exchanged for an unscoped token and adds targeted tests.

CVSS Score

VectorN
ComplexityL
PrivilegesL
User InteractionR
ScopeU
ConfidentialityH
IntegrityH
AvailabilityN
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:N

Vulnerability Location

SourceLine 1089
src/zenml/zen_server/pipeline_execution/utils.py
build_runner_environment()
SinkLine 549
src/zenml/zen_server/routers/auth_endpoints.py
api_token()

Source-to-Sink Analysis

1
src/zenml/zen_server/pipeline_execution/utils.py:1089

ZenML injects a signed API token carrying a run, schedule, or deployment binding into the workload environment.

PYTHON
environment["ZENML_STORE_API_TOKEN"] = generate_access_token(
  user_id=user.id, pipeline_run_id=run.id
).access_token
2
src/zenml/zen_server/routers/auth_endpoints.py:549

During the live run, malicious workload code asks the generic-token branch for the maximum allowed lifetime.

PYTHON
GET /api/v1/api_token?token_type=generic&expires_in=604800
Authorization: Bearer <workload-token>
3
src/zenml/zen_server/auth.py:1093

The vulnerable branch issued a new token with only the parent user ID, removing every workload-liveness claim.

PYTHON
return generate_access_token(
  user_id=token.user_id,
  expires_in=expires_in,
).access_token

Impact Analysis

Critical Impact

The workload escapes its intended authorization lifetime and continues authenticating as the issuing user or service account after termination. Accessible data and mutations depend on that principal's existing RBAC grants; the flaw does not add permissions beyond them.

Attack Surface

ZenML servers that inject API tokens into pipeline workloads and expose the authenticated API-token exchange endpoint.

Preconditions

Attacker-controlled code or a compromised dependency must execute in a live credential-bearing workload. No ZenML signing key, user password, or pre-stolen external token is needed.

Proof of Concept

Environment Setup

Run an isolated affected ZenML server and execute a test pipeline step that receives ZENML_STORE_API_TOKEN.

Target Configuration

Use normal authentication and default generic-token lifetime limits. The parent token should carry a real pipeline_run_id.

Exploit Delivery

During the run, request a generic token with the injected credential. End the run, verify the parent is rejected, then call a harmless authorized read endpoint with the child token.

Outcome

PR #5152 rejects the scope-stripping exchange, so a workload-bound parent cannot mint an unscoped generic child token.

Expected Response: On the vulnerable build, the child request remains authenticated after the parent workload credential has expired through run-liveness enforcement.

Run this level of analysis on your repo.

Winfunc traces source-to-sink paths, validates exploitability, and gives your team patch-ready remediation.

Vulnerability Detection