Radar · Rust · #001 · Week 39 · Sep 21 – 27, 2026
GitHub Actions leaking secrets when Miri output is cached
Worth knowingtooling
- What changed
- The Rust Security Response Team found that
cargo miristored all environment variables intarget/. With GitHub Actions cachingtarget/, a pull request could read secrets from the cache. From the 2026-09-22 nightly, Miri keeps onlyCARGO_*variables (exceptCARGO_*_TOKEN) andOUT_DIR. - Production impact
- You were exposed if CI runs
cargo miri, that step can see secrets as environment variables, the job cachestarget/(for example with actions/cache or Swatinem/rust-cache), and pull requests can read that cache. After fixing the job, the advisory says to clear the cache and consider rotating any secrets that may have leaked. - Try it
- Find every CI job that runs
cargo miriand cachestarget/: disable the cache for it or scope secrets to steps that do not run Miri, then clear the cache. - Source
- blog.rust-lang.org/2026/09/21/github-actions-leaking-secrets-when-miri-output-is-cached/
Understand it, then run it
Miri is a tool that runs your Rust tests in an interpreter to catch undefined behaviour. To do its job it runs several times per test, and it needs to remember some settings between those runs, so it saved the environment variables it saw into files under target/.
The trouble is that it saved all of them. Many CI setups cache target/ to build faster, and pull requests can often read that cache, so a secret such as an API token could end up readable by anyone who can open a pull request. The fixed Miri saves only the variables it needs.
Run it now
// The fix in the Rust security advisory: Miri used to write every environment
// variable it saw to target/, where a cached target/ could hand it to a pull
// request. It now keeps only CARGO_* variables (except CARGO_*_TOKEN) and
// OUT_DIR. This applies both rules to the environment of a typical CI step.
fn kept_after_fix(name: &str) -> bool {
(name.starts_with("CARGO_") && !name.ends_with("_TOKEN")) || name == "OUT_DIR"
}
fn main() {
let step_env = [
"CARGO_PKG_NAME",
"CARGO_TERM_COLOR",
"CARGO_REGISTRY_TOKEN",
"OUT_DIR",
"GITHUB_TOKEN",
"AWS_SECRET_ACCESS_KEY",
"PATH",
];
println!("{:<24} {:<18} {}", "variable", "before the fix", "after the fix");
for name in step_env {
let after = if kept_after_fix(name) { "written" } else { "not written" };
println!("{name:<24} {:<18} {after}", "written");
}
}
What it printed when we ran it on Rust 1.98.1 (edition 2024)
variable before the fix after the fix CARGO_PKG_NAME written written CARGO_TERM_COLOR written written CARGO_REGISTRY_TOKEN written not written OUT_DIR written written GITHUB_TOKEN written not written AWS_SECRET_ACCESS_KEY written not written PATH written not written
Run sends this program (for Solidity, the contract and its tests) to our own sandbox, where it is compiled and run once, with no network, and what it printed or the test report comes back here. Nothing is kept. Runs are counted per visitor for the day so everyone gets a turn; the details are on the legal page.
Written by gpt-oss-120b from the linked source · claims checked against the sources · human-reviewed