GitHub Actions leaked secrets via cached Miri output
On 21 September 2026, the Rust Security Response Team published a disclosure explaining how a routine performance optimization in Rust continuous integration could turn into a secret-leak path. The issue was not a direct vulnerability in Miri by itself, but a dangerous interaction between three common ingredients: GitHub Actions caching, a workflow that caches the Rust target/ directory, and cargo miri storing environment variables in build output so it can reuse them across Miri invocations. If a job had access to secrets and also wrote a cache containing those stored variables, a later pull request run could read the cache and extract the secrets.
The incident is a useful case study because it sits at the boundary between language tooling, CI configuration, and platform security assumptions. Rust projects often cache aggressively to keep test times reasonable, and Miri is a valuable tool for detecting undefined behavior in unsafe code. GitHub Actions, for its part, makes caches easy to share across workflow runs. None of those choices are inherently wrong. The failure mode appears when secrets are treated as ordinary environment variables in a job that can persist state into a shared artifact.
This post explains what Miri does, how the cached-output path worked, what the risk model looked like, and what maintainers should change in their workflows. The goal is practical: if you run Miri in GitHub Actions, you should be able to tell whether you were exposed and how to reduce the chance of a similar leak in the future.
What Miri is and why teams run it in CI
Miri is an interpreter for Rust's Mid-level Intermediate Representation (MIR). Instead of compiling a program to native code and executing it normally, Miri runs the program in an interpreter that can detect classes of undefined behavior and other problematic behavior that ordinary tests may miss. The Miri project describes itself as an undefined-behavior detection tool for Rust and notes that it can run binaries and test suites from Cargo projects.
In practice, Miri is useful for crates that contain unsafe code, FFI boundaries, low-level data structures, or subtle aliasing and initialization patterns. It can catch issues such as out-of-bounds accesses, use-after-free, invalid uninitialized data, alignment problems, and violations of aliasing models, depending on configuration. Because those checks are expensive compared with ordinary unit tests, teams often run Miri on selected test suites or in scheduled jobs rather than on every local edit.
That cost is the reason caching enters the picture. A Rust build directory can contain compiled dependencies, incremental artifacts, and tool-specific outputs that are time-consuming to recreate. GitHub Actions workflows frequently use the actions/cache action or Rust-oriented wrappers such as Swatinem/rust-cache to restore and save parts of ~/.cargo and target/. When a Miri job is slow, caching target/ can look like an obvious win. The Rust disclosure shows why that convenience needs to be evaluated against what the job can see.
How cargo miri ended up storing environment variables
The Rust blog post explains a specific implementation detail: when cargo miri is invoked, Miri needs to retain build-relevant environment variables between runs. The reason, as noted in the disclosure, is that Miri is invoked multiple times by cargo miri for complicated reasons. To make that work, the code stored environment variables to target/.
The crucial detail is scope. According to the disclosure, the implementation stored all environment variables, not only a narrow set of build-related variables. That means any secret made available to the environment of the job or step running Miri could be written into the target/ directory. Examples include repository secrets passed through env:, deployment tokens, package registry credentials, cloud credentials, or any other sensitive value exposed to the process.
Caching target/ is a normal Rust CI pattern, but it changes the lifetime and visibility of everything inside that directory. A file that would otherwise disappear when the runner is destroyed can become a cache entry that another workflow run restores. The disclosure's short-term fix narrowed what Miri preserves: it now keeps CARGO_* environment variables except CARGO_*_TOKEN, plus OUT_DIR. That reduces exposure for common secret names, but it does not remove the broader lesson that build outputs and caches should not be assumed safe for arbitrary secrets.
The disclosure links to the affected Miri code path and to pull request rust-lang/miri#5337 as the short-term remediation. It also notes that the patched behavior may not have been available on nightly immediately at publication time, and that the Miri release in the upcoming nightly (2026-09-22) would no longer have the problem. For maintainers, that timing detail matters: a workflow can remain exposed until it runs a fixed toolchain and its old caches are removed.
Why GitHub Actions caching creates a sharing boundary
GitHub Actions caching is designed to speed up workflows by reusing files that are expensive to compute. The cache action restores files to specified paths when a key matches, and it can create a new cache when a job completes successfully. The GitHub documentation also warns that sensitive information should not be stored in files in the cache path, because anyone with read access can create a pull request on a repository and access cache contents. It further notes that forks of a repository can create pull requests on the base branch and access caches on the base branch.
The Rust disclosure describes the practical consequence for typical repository setups. CI runs on main and other branches often have permission to write caches, while pull request runs can read from caches. That read access is useful for performance: a PR should not need to rebuild every dependency from scratch. But it also means cache contents become data that untrusted or less-trusted code paths may be able to observe.
The attack path does not require breaking GitHub's cache isolation model in the abstract. It relies on a repository configuration where a privileged or secret-bearing run writes sensitive data into a cache, and a later PR run can restore that cache. Once the data is present in restored files, the PR job can inspect it with ordinary shell commands, test code, or build scripts. The leak happens at the boundary between "build artifact" and "shared CI state."
This is why cache safety cannot be evaluated by looking at the cache key alone. Teams often focus on key design to avoid cache poisoning or stale dependencies. The Miri incident shows the opposite problem: even when the cache is functioning as intended, the contents may be too sensitive to share.
The risk model described in the disclosure
The Rust Security Response Team outlined a plausible exploitation path. Pull request CI can be triggered by anyone who can open pull requests on a repository. GitHub requires maintainer approval for the first PR from a contributor in some situations, but future PRs from someone who has previously landed a change can rerun CI on every push. That means a person with an established contribution path may be able to trigger workflows that read from caches written by earlier runs.
In an affected setup, the attacker would aim a PR job at the cached target/ contents produced by a Miri run that had secrets in its environment. The job could extract the stored environment variables, exfiltrate them through any channel available to that workflow context, and then push another commit to reduce the visibility of the malicious change. The disclosure notes that GitHub sometimes hides overwritten commits in its UI and that CI run logs and overwritten commits are deleted after a few months. Those behaviors do not create the leak, but they can make post-incident detection harder.
The report also states that the team performed an ecosystem scan of GitHub repositories. It identified one repository with the issue and seven repositories that did not appear vulnerable but should be cautious anyway. The maintainers were contacted. The disclosure recommends that projects check their own GitHub Actions setups, because such a scan may be imperfect.
The affected conditions are concrete. A repository was vulnerable, according to the disclosure, if it ran cargo miri in CI; the step running Miri had access to secrets as environment variables, either directly, through workflow-level env, or through a previous step that persisted secrets into the environment; the workflow cached the target/ directory, commonly with actions/cache or Swatinem/rust-cache; and the cache was accessible to pull requests. That checklist is a good starting point for incident response because each condition suggests a control point.
Immediate actions for maintainers
If a repository matches the affected pattern, the first priority is to stop further exposure and assess whether secrets may already be present in caches. The Rust disclosure lists several quick fixes: disable the cache for the affected job, scope secrets to steps that do not call Miri, or temporarily disable Miri. After making a workflow change, maintainers should clear the relevant cache entries. The disclosure also advises considering rotation of any secrets that might have leaked.
Cache deletion matters because updating a workflow does not erase old cache data. A vulnerable job may have already written a cache entry under an existing key. If that entry remains accessible, later runs could still restore it. Clearing caches removes the known artifact; rotating secrets limits the value of any data that was already copied out.
Secret rotation should be guided by exposure, not panic. Review which secrets were available to the affected job: repository secrets, organization secrets, environment secrets, cloud credentials, package tokens, signing keys, or API tokens passed as environment variables. If a secret was not in scope for that job, it is less likely to have been written by Miri's old behavior. If a secret was present, treat it as potentially exposed until you have evidence otherwise. The right response depends on the secret type: revoke and replace tokens, rotate credentials, invalidate sessions where possible, and audit provider logs for suspicious use.
Upgrading to a fixed Miri toolchain is also necessary, but it is not sufficient on its own. The disclosure states that the upcoming nightly would no longer have the problem. Projects pinned to older nightlies, cached toolchains, or custom CI images may continue running the vulnerable behavior. A complete response includes the fixed tool, clean caches, narrowed secret scope, and an audit of workflow permissions.
Practical mitigation guidance for GitHub Actions workflows
The safest default is to treat caches as data that may eventually be readable by code you do not fully trust. That does not mean "never cache." It means cache paths and cache-producing jobs need the same scrutiny as artifacts, logs, and build outputs.
Start by separating secret-bearing work from cache-writing work. If a job needs deployment credentials, publishing tokens, or access to sensitive APIs, avoid giving that same job broad permission to save target/ or other directories that tools may populate unpredictably. Scope secrets to the smallest possible step. Do not expose a secret through workflow-level env unless every step in that workflow truly needs it. If a previous step needs a secret, avoid persisting it into shell profiles, files, or global environment state that later steps inherit.
Be careful with caching target/ wholesale. The target directory is convenient, but it is a mixed bag of compiler outputs, dependency artifacts, build-script outputs, and tool-specific files. A narrower cache can reduce both correctness bugs and security surprises. Consider caching only dependencies that are safe and clearly understood, or using a Rust cache action with settings that match your trust model. Even then, remember that "safe by default" is not the same as "safe for jobs with secrets."
Review every job that can write to a cache and ask a simple question: could this job see any value that must never be disclosed to a pull request author or to code running in a PR context? If the answer is yes, redesign the job. The Rust disclosure generalizes this point beyond Miri: even if you do not run Miri, jobs that can write to public caches should not have access to secrets, because many tools assume the whole environment can be written to disk.
For Miri specifically, a conservative configuration is to run Miri in a dedicated job without production secrets and without saving target/. If the runtime cost is too high, evaluate whether restoring dependencies is sufficient and whether saving Miri-specific outputs is worth the risk. The fixed Miri behavior reduces one known path, but it should not be treated as a guarantee that future tools will never persist sensitive state in a cached directory.
Threat-model lessons beyond this incident
The disclosure's threat-model section is important because it avoids overfitting the response to one tool. Rust, Cargo, and Miri do not guarantee that environment variables will always be safe from being copied into target/. The team treated the issue seriously and patched it, but maintainers should not build a security model around the assumption that every tool will perfectly sanitize its environment before writing build state.
Build scripts are another source of risk. A crate's build.rs can read environment variables, fetch network resources, generate files, and influence compilation outputs. If a build script captures a token or writes sensitive data into generated artifacts, those artifacts may end up in target/. The same concern applies to test harnesses, code generators, proc-macro support files, and custom tooling wrapped around Cargo.
GitHub Actions adds its own sharp edges. Caches are not private encrypted vaults for arbitrary runtime state; they are performance artifacts with documented access rules. Workflow YAML can make secrets available broadly with a single env: block. Reusable workflows, composite actions, and third-party actions can obscure where secrets are consumed. A repository that "only" passed a token to publish a crate or access a private dependency may accidentally make that token visible to unrelated steps.
The right mental model is to classify CI data paths. Secrets belong in secret stores and should flow only to steps that need them. Caches belong to performance engineering and should contain reproducible, non-sensitive data. Logs, artifacts, and uploaded debug bundles should be reviewed with the same discipline. When those categories mix, a bug in any one layer can become a disclosure in another.
A checklist for reviewing Miri workflows
Before reenabling or keeping Miri in CI, walk through the workflow with the disclosure's conditions in hand. First, identify every cargo miri invocation and the job that contains it. Second, list every secret available to that job, including repository, organization, and environment secrets, and note whether they are set at workflow, job, or step scope. Third, inspect cache usage: actions/cache, Swatinem/rust-cache, custom cache scripts, and any action that saves target/ or related directories.
Fourth, determine who can trigger the workflow and whether the resulting cache is readable by pull requests or other less-privileged contexts. Fifth, check toolchain versions. A fixed Miri release removes the known behavior, but old nightlies or stale environments can persist. Sixth, clear caches created before the fix or before the workflow was narrowed. Seventh, decide whether any exposed secret needs rotation and record that decision.
Finally, test the new workflow shape. A secure workflow that is too slow may tempt maintainers to reintroduce broad caching later. If Miri remains expensive, consider running it on a smaller test matrix, on nightly schedules, on protected branches, or only for crates with unsafe code. Performance controls are easier to maintain than emergency credential rotation.
Conclusion
The cached Miri output incident is a reminder that CI systems compose behaviors, not intentions. Miri needed build-relevant state across invocations, Rust projects needed faster builds, and GitHub Actions provided a cache mechanism that is intentionally reusable. The resulting leak path came from storing more environment data than necessary in a directory that many workflows treat as a harmless performance cache.
Maintainers should respond at two levels. The narrow response is to check for the affected Miri and GitHub Actions pattern, upgrade to a fixed Miri toolchain, clear caches, and rotate secrets if exposure is plausible. The broader response is to treat every cache-writing job as sensitive: keep secrets out of those jobs, avoid caching whole build directories by default, and assume that files written during CI may be restored somewhere you did not originally expect.
That approach will not make CI simple, but it makes failures less surprising. The next leak may not involve Miri, target/, or Rust at all. The same design question will still apply: if this job can see a secret, where can its files go next?
Sources
- GitHub Actions leaking secrets when Miri output is cached — Rust Blog
- Dependency caching reference — GitHub Actions documentation
- Miri: An interpreter for Rust's mid-level intermediate representation — rust-lang/miri
- Miri pull request #5337 referenced by the Rust disclosure