Miri Was Leaking CI Secrets Through GitHub Actions Caches: What to Clean Up
CI/CD pipelines are a common attack surface, and most developers focus on the obvious risks: exposed tokens, world-readable repos, overly permissive workflow permissions. But a subtler class of vulnerability comes from c
CI/CD pipelines are a common attack surface, and most developers focus on the obvious risks: exposed tokens, world-readable repos, overly permissive workflow permissions. But a subtler class of vulnerability comes from caching, and in September 2026 the Rust project published an advisory that is a textbook case. Miri, Rust's interpreter for detecting undefined behavior, was writing every environment variable it saw into the target/ directory. Cache target/ in GitHub Actions, as most Rust projects do, and any secret handed to that job was sitting in the cache where a later pull request run could restore it and read it.
The good news first, because it changes how you should read the rest: this is fixed. The Rust project published the advisory on 21 September 2026 and the nightly toolchain dated 22 September 2026 stops Miri persisting arbitrary environment variables, keeping only CARGO_* (excluding CARGO_*_TOKEN) and OUT_DIR. No CVE was assigned. What is left for you is cleanup rather than defense: update the toolchain, delete the caches you already have, and rotate anything that was in scope. This post covers the mechanism, that cleanup, and how to stop caching biting you again.
💡 Why This Matters
Miri runs Rust programs in an interpreted environment so it can detect undefined behavior at runtime. To notice when something build relevant changed between runs, cargo miri recorded the environment it was invoked with, and it recorded all of it rather than only the variables it needed. That record lived inside the target/ directory. Note what is not required here: nothing in your code, your test output or your logs had to mention the secret. Passing it to the step was enough for it to land on disk in the directory everyone caches.
That matters because of how GitHub Actions cache scoping works. A workflow run can restore caches created on its own branch and on the default branch, so a pull request run can pull back a cache that a privileged push to main created. The advisory spells out the conditions: you are affected if you run cargo miri in CI, pass secrets as environment variables to that step, cache the target/ directory with something like actions/cache or Swatinem/rust-cache, and allow pull requests to read from that cache.
The broader lesson goes beyond Miri. Any tool that snapshots its invocation environment into a build directory is a candidate for this class of bug, and build directories are exactly what CI caching is pointed at. Rust's toolchain is just where this instance was caught, reported and documented.
🧰 Prerequisites
- A GitHub Actions workflow that uses Miri (typically via
cargo miri test) - Familiarity with the
actions/cacheaction or the built-in Cargo caching patterns - Repository secrets configured in Settings, such as API keys or tokens passed to your test environment
🔍 How the Leakage Path Works
Here is the setup that creates the risk. A workflow injects a secret as an environment variable, runs Miri, and caches the Cargo target/ directory to speed up future runs. The target/ directory is the part that matters. Caching the registry or the Miri sysroot was never the problem.
# Vulnerable pattern (simplified)
- name: Run Miri
env:
MY_SECRET: ${{ secrets.MY_SECRET }}
run: cargo miri test
- name: Cache build output
uses: actions/cache@v4
with:
path: target
key: miri-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
The value of MY_SECRET does not have to appear in any log or test result. cargo miri persisted it into target/ as part of the environment snapshot, and the cache step then packaged target/ up. The next job or pull request run that restores this cache has a file containing your secret, without that secret ever being granted to the run.
No exotic trigger is required. You do not need a panic, a crash, or verbose diagnostics. Passing the secret to the step and caching target/ is the whole bug. The advisory also notes that someone who lifted a secret this way could push further commits to cover their tracks, so a clean-looking workflow log is not evidence you were fine.
🛡️ Step-by-Step: Hardening Your Pipeline
Step 1: Never inject production secrets into Miri test runs. If your tests genuinely need a credential, use a scoped, revocable test credential with no real permissions. Treat anything Miri can see as potentially loggable.
# Prefer a dummy or scoped value for Miri runs
- name: Run Miri
env:
MY_SECRET: "dummy-value-for-miri"
run: cargo miri test
Step 2: Fix your caching scope. The directory that leaked is target/, so either stop caching it on jobs that see secrets, or stop giving those jobs secrets. If you do cache something for Miri, cache the sysroot, and use a distinct cache key so a Miri cache never collides with a standard build cache.
- name: Cache Miri sysroot only
uses: actions/cache@v4
with:
path: ~/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib
key: miri-sysroot-${{ runner.os }}-${{ hashFiles('rust-toolchain.toml') }}
Check the exact sysroot path for your toolchain with rustc --print sysroot rather than hardcoding it. The path above is illustrative; confirm it in your environment.
Step 3: Restrict workflow permissions explicitly. Add a top-level permissions block to limit what a workflow can do even if a cache is poisoned.
permissions:
contents: read
Step 4: Isolate Miri jobs from secret-bearing jobs. Use separate jobs so the job running Miri never sees a real secret. GitHub Actions does not share environment variables across jobs unless you pass them explicitly, so inject secrets at the step level in the job that actually needs them. One correction worth making if you copy patterns off the internet: secrets: inherit is only valid on a job that calls a reusable workflow with uses, not on a job that runs its own steps.
jobs:
miri:
runs-on: ubuntu-latest
# No secrets injected here
steps:
- uses: actions/checkout@v4
- run: cargo miri test
deploy:
runs-on: ubuntu-latest
needs: miri
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
steps:
- run: ./deploy.sh
Step 5: Audit your cache keys and paths regularly. Run a search across your workflow files for any actions/cache or cache-dependency-path usage that overlaps with paths your secret-injected steps write to.
grep -r "actions/cache" .github/workflows/
🔧 Troubleshooting
My Miri tests need real environment values to run correctly. Refactor the tests to accept injectable fakes, or use a secrets manager that vends short-lived credentials scoped only to the test run. This is a design problem as much as a security one.
I am not sure what is in my cache. Download a cache artifact locally using the GitHub CLI (gh cache list and gh cache download) and inspect the contents before assuming it is clean. The official GitHub CLI docs cover the exact flags.
We use a third-party caching action, not actions/cache. The same principles apply. Check whether that action stores artifacts in a location your Miri run can write to, and review the action's own permissions model.
✅ Wrap Up
Caching is one of the best levers you have for speeding up CI, but it introduces a persistent artifact that outlives a single workflow run. In this case the artifact was the ordinary Cargo target/ directory, and the tool filling it with secrets was doing so as a convenience feature. Be deliberate about what goes into the cache and which jobs hold real secrets.
Concretely, for this advisory: move to a toolchain from 22 September 2026 or later, delete the existing caches on any repository that ran cargo miri with secrets in scope, and rotate those secrets. Then, as standing practice, keep Miri jobs secret free, lock down workflow permissions, and audit your cache paths as part of any security review. Treat cached artifacts with the same suspicion you would treat a shared filesystem.
The primary source is the Rust project advisory GitHub Actions leaking secrets when Miri output is cached, published 21 September 2026. Read it in full for the exact affected conditions and for any follow-up, and pair it with the GitHub Actions security hardening guide in the GitHub docs.
Related from the lab
- Better Homelab Secret Management with Infisical
- An AI Agent Ran Up a $6,531 AWS Bill Scanning DN42. Here's How to Cap Yours
- The Design of Everyday Cryptography: A Homelab Primer
Originally published at codewithromi.com.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.