Skip to content

Job runtime JWT is accepted as a generic runner-protocol credential #155

Description

@Bnjoroge1

Severity

P0 / critical security

Review baseline

0bd0e314696e520aa47620cbe8f008d010e01e57

Summary

require_protocol_bearer authorizes the system token or any valid locally signed JWT, without checking the token capability/scope. Job runtime JWTs are deliberately exposed to workflow steps as ACTIONS_RUNTIME_TOKEN, so a job credential can authenticate to runner-protocol endpoints that are not job-scoped.

The protected router includes destructive and cross-identity operations such as runner deletion, session creation/deletion, job completion, timeline/log mutation and broker operations.

Example source-derived attack

A workflow step holding its own ACTIONS_RUNTIME_TOKEN can authenticate to a path such as:

DELETE /_apis/distributedtask/pools/1/agents/{victim_runner_id}
Authorization: Bearer <job runtime JWT>

require_protocol_bearer accepts the token solely because its HMAC verifies. The handler then trusts the runner id in the URL and can purge that runner identity. The guest control socket permits the /_apis/... protocol surface, so this is reachable from inside runner VMs.

The same capability-confusion pattern affects other runner/session/reporting routes that rely on the generic protocol gate rather than an exact typed identity.

Impact

A compromised or intentionally malicious job may be able to:

  • terminate or deregister sibling runners;
  • disrupt/requeue sibling jobs;
  • create/delete sessions outside its own identity;
  • mutate another job's timeline/log/reporting state if identifiers are learned;
  • call broker/runner-management surfaces with a credential intended only for its own job runtime.

Recommended remediation

Replace generic local-JWT acceptance with typed capabilities, e.g.:

SystemIdentity
RunnerManageIdentity
RunnerListenIdentity { runner_id }
JobRuntimeIdentity { plan_id, job_id }
DebugWorkerIdentity { job_id }

Every handler should require the narrowest identity type and verify that path/body runner, session, plan and job identifiers match it. A valid signature should establish who issued the credential, not grant access to every protocol endpoint.

The existing debug-session split between WorkerJob and JobRuntimeIdentity is a good pattern to extend.

Acceptance criteria

  • A job runtime token is rejected by runner-management and runner-listen endpoints.
  • A runner-listen token cannot mutate another runner's identity/session.
  • Job-scoped reporting endpoints reject mismatched plan/job ids.
  • All protocol aliases have explicit capability tests, including guest SocketSurface access.
  • Negative tests prove a workflow step cannot deregister another runner with ACTIONS_RUNTIME_TOKEN.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions