Map
โ†‘Supply Chain Security

CI Runner Token Extraction

Wiki conceptsecuritygithub-actionsci-cdoidc โ†ณ show in map Markdown
title
CI Runner Token Extraction
type
concept
summary
Code on a CI runner reads the runner-process memory to extract OIDC tokens for direct registry use
tags
security, github-actions, ci-cd, oidc
created
2026-05-12
updated
2026-05-12

A class of attack where malicious code that has gained execution on a CI runner reads the runner-host process's memory directly to pull out an OIDC token โ€” and then uses that token to authenticate against a downstream service (npm, PyPI, AWS via federated identity) outside the workflow's declared publish steps.

This is the link that turns "code execution on the runner" into "wrote to the production package registry."

How it works (GitHub Actions specifics)

GitHub-hosted runners run a Runner.Worker process that handles workflow execution. When a workflow declares id-token: write and a step requests an OIDC token (getIDToken() in @actions/core, the implicit call inside Trusted-Publishing-aware steps, etc.), the runner mints a JWT lazily and holds it in memory.

If attacker code is on the same machine โ€” running as the same user as the worker, which it is by default โ€” it can:

  1. Locate the Runner.Worker PID via /proc/*/cmdline
  2. Read /proc/<pid>/maps to find the worker's address space layout
  3. Read /proc/<pid>/mem to dump that memory
  4. Pattern-match for a JWT (eyJโ€ฆ base64 prefix is a fast signal)
  5. POST the JWT to the registry's OIDC-trusted-publisher endpoint

The token is valid until expiry โ€” usually minutes โ€” and any publish that lands inside that window is indistinguishable from a publish the legitimate Publish Packages step would have made. The registry sees a workflow with a valid binding asking to publish; it has no per-step provenance to consult.

The same memory-dump script (verbatim, with attribution comment) has been observed in:

  • tj-actions/changed-files compromise, March 2025 โ€” original publication via a force-pushed tag on an unpinned action.
  • TanStack @tanstack/* compromise, May 2026 โ€” see tanstack-npm-supply-chain-postmortem.

The script's authors call this technique [out of scope for the registry to defend against] โ€” and they're right. The defense has to be on the workflow side.

Why the workflow's Publish Packages step doesn't matter

A common false sense of safety: "Our publish step has a manual approval / a separate environment / a hardware-key-signed step." None of that matters if the OIDC token is mintable from any job in the workflow with id-token: write. The attacker doesn't need to enter your publish step. They mint the token themselves and POST directly to the registry.

The TanStack incident is the worked example. The legitimate Publish Packages step was skipped because tests had failed. The publishes still happened โ€” from the test/cleanup phase, where attacker binaries ran after the poisoned cache was restored.

Defenses

The strong fix is separation of execution from token possession.

Move publish to a job that runs no contributor code.

  • A typical safe shape: job 1 builds and produces an artifact (uploaded via actions/upload-artifact). Job 2 has id-token: write, runs no third-party deps, downloads the artifact, verifies it against a hash manifest signed elsewhere, and publishes. If job 2 has nothing in its environment that can run attacker code, no in-memory token can be reached.

Minimize id-token: write to one job.

  • Set permissions: {} at the workflow level and add id-token: write only in the publish job. Default-deny per job.

Don't restore caches in the publish job.

  • The publish job should not invoke actions/cache@vโ€ฆ for any path attacker code could have written. Pull dependencies fresh in that job, or restore from a cache scope no PR-triggered run could have written to. This breaks the github-actions-cache-poisoning โ†’ token-extraction bridge.

Pin all actions to commit SHAs.

  • A floating @vโ€ฆ tag on a third-party action is itself an extraction vector (the tj-actions/changed-files mechanism). pinact and Dependabot's pinned mode automate this.

Provenance-source-verification (registry-side, ecosystem-wide).

  • The structural fix is for registries to record which workflow file path and step ID produced the publish, and let publishers declare which step is allowed to mint. This is being discussed; it isn't shipped. Until it is, the publisher-side mitigations above are the whole story.

See also