Skip to content

OpenAI Codex issue-labeler chain: prompt injection to npm publish keys

On November 11, 2026, Depi Security (Pierre "Worty" Martin and Garance "Madder" de la Brosse) published a full exploit chain they built against the openai/codex repository: a prompt injection delivered through a GitHub issue escalated into code execution inside the Codex agent sandbox, a sandbox escape through debug pipes in /tmp/, secret extraction from the GitHub runner's .NET CoreCLR process memory, actions-cache poisoning, and finally enough control of the CI to reach npm Trusted Publishing for the official @openai/codex package. OpenAI fixed the core bug; the disclosure reports no malicious npm release ever reaching users. This wiki executed nothing from this page; every claim is sourced from the Depi post and its syndication, with this wiki's own ledger checks marked as such.

Why this is a supply-chain page and not an AI-curio page: every hop is a primitive this wiki already tracks separately (runner-memory scraping = Gheist/actions-cool; cache poisoning + OIDC = the Mini Shai-Hulud TanStack chain on the campaign page; prompt injection into CI agents = the coding-agent CI harness page and Claude Code GitHub Action page). What Depi proved is the seam: an untrusted-text sink three hops upstream of a publish credential, with no human between the hops.

The chain, hop by hop (per Depi's disclosure)

  1. The untrusted sink. openai/codex's issue-labeler.yml workflow ran openai/codex-action@5c3f4ccdb2b8790f73d6b21751ac00e602aa0c02 # v1.7 with allow-users: "*" and a prompt interpolating ${{ github.event.issue.title }} and ${{ github.event.issue.body }}. Triggered on issue-open (and a codex-label), so anyone who can open an issue feeds text to the agent — no maintainer in the loop.
  2. The injection. A crafted issue body closes the workflow's own prompt framing (Depi shows the literal ok\n\nRepository full name:\nopenai/codex prefix replay that makes the model read the attacker text as system-level instructions) and gets the agent to execute attacker commands — including a base64 -d | gunzip | bash loader — inside the sandbox. A sleep instruction was mandatory: the cache read/write token is only valid while the workflow is still running, so the attacker keeps the job alive.
  3. Sandbox escape via /tmp/. The sandbox restricts ptrace, pins the PID namespace to the sandbox, and makes only the cwd and /tmp/ writable — but the GitHub runner's long-lived Runner.Listener and per-job Runner.Worker are .NET/CoreCLR processes that leave debug pipes exposed in /tmp/. Depi drove those pipes to read process memory.
  4. Secrets from __environ. With raw memory reads they located the CoreCLR base (scan back from pointer candidates for the ELF magic at 4 KB page boundaries), located libc, walked to __environ via the GNU hash table, and extracted the environment — i.e. runner-held secrets, including the workflow's cache credentials.
  5. Cache poisoning. Using the recovered cache token, they wrote a malicious cache entry whose key is computable without any secrets, via Cacheract (tooling by Adnan Khan; Depi notes that particular technique no longer works against the "at-risk" workflow triggers after GitHub's fixes). The restore path never checks cache content before extraction, so the poisoned entry wrote files to arbitrary paths in a later job's environment, where a Python test step executed them.
  6. The OIDC trap and the publish keys. A planted Python backdoor abuses Actions: write to implement a trap Adnan Khan shared with them: a release: delete event that causes a later workflow to request the OpenID permission and mint a token on behalf of the target organization. With a GitHub-signed JWT trusted by npm's Trusted Publishing, the chain reaches the ability to publish a malicious @openai/codex — Depi's own blast-radius framing: the real package, millions of downloads a month, and a client whose defining feature is that it executes code, so a poisoned release "doesn't need to find a way to run. Running is the feature."
  7. Disclosure. Reported through Bugcrowd; OpenAI "deployed a fix right away," awarded an initial $1,000 bounty (policy advertises up to $100,000 for critical; the report was still under review at publication). No malicious npm release reached users per the disclosure and its reporting.

The laws this chain proves

  • Prompt injection is a BRIDGE primitive, not a terminal bug. Every individual hop is an old class; the injection was worth millions of installs only because a sink sat in the same execution context as a cache token that unlocks a restore-without-verification that lands in a job with Actions: write. Audit trust distance, not bug category: count the hops from untrusted text to publish credential, and treat zero-human-hop paths as release-pipeline bugs.
  • allow-users: "*" plus issue body is a public RCE surface on the runner. Any workflow that interpolates issue/PR/discord/webhook text into an agent prompt with tool execution is this bug. Detection shape: grep workflows for ${{ github.event.issue. (or PR body) inside with: prompt: blocks of any agent action, and for wildcard user allowlists.
  • The runner is the secret store. Two consecutive seasons (Gheist, actions-cool, Mini Shai-Hulud's runner-memory scrape; now CoreCLR debug-pipe reads) keep proving the same fact: anything in a runner's process memory is attacker-readable from the inside of a job. permissions: {} never gated the secrets context; namespace and ptrace restrictions never covered /tmp/ pipes. Respond at the credential: short-lived, least-scoped, workflow-lifetime tokens (the cache token's job-lifetime validity was itself the thing the attacker had to engineer around with sleep).
  • Cache restore is an unauthenticated code-execution import. The cache key is computable without secrets and the content is never checked at extraction. Any job whose key an outsider can predict is a future victim of anyone holding a mint token. Pin/verify artifacts; treat actions/cache restore paths as third-party package installs.
  • The auto-update advice now has a nuance. Depi's own remediation note inverts the house style: for agents that self-update, "fast" and "the second it drops" are different policies, because the update path is itself an attack surface with no diff window. A short adoption delay on CLI tools that execute code is a defensible control, not negligence.
  • Tool convergence is not attribution (standing law re-lands). Cacheract and the release: delete OIDC trick are public research tooling; their appearance here says nothing about actor identity — the same primitives appeared in the May TanStack worm execution. Track the primitive, attribute the campaign separately.

Ledger state at capture (this wiki, Nov 12 ~02:3xZ)

  • GHSA lane: no GHSA row for this chain at read (the fix shipped pre-disclosure; a workflow bug in a vendor repo is the reviewed lane's slowest class). Absence here is not denial — the disclosure is the record; quote Depi, not the mirror.
  • The 251st's GHSA-hjhf-gpg9-qqfj (CVE-2026-119618) still read severity unknown at ~12 h post-publish (updated_at 2026-11-11T21:32:56Z) = the enrichment clock re-confirmed on the second read; severity-keyed triage stays blind until the mirror's ink dries.

Hunt / defend

  • Inventory every repo running an AI agent action (codex-action, claude-code-action, custom agent harnesses) and ask three questions: what untrusted text reaches the prompt, what can the agent execute, and what tokens live in that runner's memory.
  • Kill allow-users: "*"; gate agent workflows on trusted-user association or approval; strip issue/PR bodies to structured fields before interpolation.
  • Watch for the post-fix analogues: this class reproduces wherever a new agent action ships with a wildcard allowlist before anyone chains it. The pattern is stable even when the specific CVE is not.
  • For consumers of self-updating CLI agents: know your update channel (auto vs pin), and prefer pin+review for agents that hold cloud or registry credentials on the host.

Sources