GitHub Actions leaking secrets when Miri output is cached
Miri 출력을 캐시할 때 GitHub Actions에서 시크릿 유출
Rust의 Miri가 빌드에 필요한 환경 변수를 보존하려고 target/에 모든 환경 변수를 저장하면서, GitHub Actions 캐시를 읽는 PR에 시크릿이 노출될 수 있었습니다. Rust 팀은 저장 범위를 줄인 수정판을 준비하고, Miri를 실행하는 CI 설정을 점검하라고 권고합니다.
- 주제
AI 요약
Rust Security Response Team은 Miri가 모든 환경 변수를 target/에 저장한다는 사실을 확인했습니다. 이 동작만으로 취약점이라고 단정할 수는 없지만, target/ 디렉터리를 GitHub Actions 캐시에 넣는 설정과 결합하면 PR이 이전 CI 실행에서 저장한 시크릿을 읽을 수 있습니다.
GitHub Actions 캐시와 PR의 관계
GitHub Actions는 실행 사이에 디렉터리를 캐시합니다. 일반적인 설정에서는 main 같은 보호된 branch의 CI가 캐시에 쓸 수 있고, PR은 캐시를 읽기만 합니다. 캐시 오염(cache poisoning)을 막기 위한 구성이지만, cargo install로 만든 바이너리나 target/ 내용을 캐시해 CI 시간을 줄이는 Rust 프로젝트에서는 다른 문제가 생깁니다.
PR CI는 저장소에 PR을 열 수 있는 사람이 실행시킬 수 있습니다. 첫 PR은 maintainer 승인이 필요하지만, 한 번 변경 사항을 반영한 사람이 올린 뒤에는 같은 PR에 새 커밋을 push할 때마다 CI가 다시 실행됩니다. 공격자는 캐시에서 정보를 빼낸 뒤 두 번째 커밋을 push해 흔적을 덮을 수 있습니다. GitHub는 UI에서 덮어쓴 커밋을 숨길 때가 있고, CI 로그와 덮어쓴 커밋도 몇 달 뒤 삭제됩니다.
Miri가 환경 변수를 저장한 이유
cargo miri는 실행 사이에 빌드에 영향을 주는 환경 변수를 유지해야 합니다. 기존 구현은 어떤 환경 변수가 실제 빌드에 필요한지 구분하지 않고 모든 환경 변수를 target/에 저장했습니다. 이 디렉터리를 캐시하면 CI에 주입된 토큰이나 다른 시크릿도 함께 남습니다. 이후 캐시를 읽는 PR이 해당 파일을 확인하면 시크릿을 가져갈 수 있습니다.
Rust 팀은 단기 수정으로 Miri가 CARGO_* 환경 변수와 OUT_DIR만 보존하게 만들었습니다. 단, CARGO_*_TOKEN은 저장 대상에서 제외합니다. 장기적으로는 Miri와 Cargo가 빌드에 필요한 환경 변수 목록을 더 정확히 전달하는 방법을 마련할 계획입니다. 수정판이 아직 nightly에 반영되지 않았을 가능성도 있으며, 문제를 해결한 Miri 릴리스는 2026년 9월 22일 예정된 nightly에 포함됩니다.
영향을 받는 조건
다음 조건이 모두 맞으면 취약한 구성으로 봅니다.
- CI에서
cargo miri를 실행합니다. - Miri를 실행하는 step이 환경 변수로 시크릿에 접근합니다. 해당 step에 직접 전달하거나 workflow의
env에 지정한 경우가 해당합니다. 이전 step이 환경에 남긴 값도 포함됩니다. actions/cache또는swatinem/rust-cache같은 action으로target/을 캐시합니다.- PR이 해당 캐시를 읽을 수 있습니다. 많은 저장소에서 의도한 동작입니다.
빠른 대응 방법은 Miri job의 캐시를 끄고, 시크릿을 Miri를 호출하지 않는 step에만 전달하고, 필요하면 Miri 실행을 잠시 중단하는 것입니다. 설정을 바꾼 뒤에는 기존 캐시를 삭제해야 합니다. 유출 가능성이 있는 시크릿은 교체하는 편이 좋습니다.
Rust 팀은 GitHub 저장소를 조사해 실제로 이 문제가 있는 저장소 1곳과 취약해 보이지는 않지만 주의가 필요한 저장소 7곳을 찾았습니다. 각 maintainer에게 연락했습니다. 다만 조사에서 빠진 저장소가 있을 수 있으므로 Miri를 실행하는 모든 프로젝트가 자체 GitHub Actions 설정을 확인해야 합니다.
더 넓은 위협 모델
Miri를 쓰지 않더라도 공개 캐시에 쓸 수 있는 job이 시크릿에 접근하지 않게 구성해야 합니다. 도구가 시크릿을 특별히 보호하지 않으면 전체 환경을 파일 시스템에 기록할 수 있습니다. target/을 캐시한다면 cargo를 호출하는 프로세스에 시크릿을 전달하지 않는 편이 안전합니다. 일반적인 cargo build와 cargo test는 대개 토큰이나 시크릿이 필요하지 않습니다.
Cargo, Miri, Rust는 환경 변수가 target/에 복사되지 않는다고 보장하지 않습니다. 공식 도구 밖에서는 build script가 환경을 컴파일 산출물에 기록할 수도 있습니다. 따라서 Miri의 저장 범위를 줄이는 수정만으로 모든 환경 변수 유출을 막지는 못합니다.
Reddit에서는 시크릿 관리 자체를 더 엄격하게 해야 한다는 의견과, 도구의 기본 동작도 안전한 방향으로 바꿔야 한다는 의견이 맞섰습니다. cc-rs처럼 환경 변수에 의존하는 도구에서는 CARGO_*와 OUT_DIR만 저장하는 수정이 충분하지 않을 수 있다는 지적도 나왔습니다. 반대로 문제의 본질은 환경 변수 기록이 아니라, 다른 job이 공유 캐시에서 그 값을 읽는 데 있다는 반론도 있었습니다.
한 댓글은 workflow를 코드 실행 전용 job과 시크릿 접근 전용 job으로 나누고, 빌드와 테스트에는 permissions: {}를 지정한 뒤 산출물만 별도 위치로 넘기는 방식을 제안했습니다. 이 구성은 캐시와 산출물 오염 문제를 모두 없애지는 않지만, 시크릿에 접근하는 job에서 임의 코드를 실행할 가능성을 줄입니다. zizmor로 GitHub Actions 설정을 점검하자는 제안도 나왔습니다.
이 문제는 OpenAI의 Predrag Gruevski가 제보했습니다. Rust 팀은 OpenAI가 제공한 Codex 접근 권한과 크레딧으로 생태계 저장소를 조사했다고 밝혔습니다.
Reddit 반응
- @u/briansmith — 아마 많은 build script 등이 모든 환경 변수를 stdout/stderr에 기록할 것입니다. 어떤 스크립트는 요청했을 때만 기록하지만,
cc-rs나 비슷한 도구처럼 계속 늘어나는 환경 변수 목록으로 설정하는 도구가 특히 그렇습니다. 애초에cargo와 대부분의 다른 명령을 시크릿이 들어 있는 환경에서 실행하는 일은 이상합니다. 이런 단계는 대개 오프라인에서 실행되므로 시크릿이 합법적으로 필요한 경우가 많지 않습니다. 다만 제 이해로는 GitHub가 해당 job의 이전 step에 노출된 시크릿을 build step이 외부로 보내지 못하게 막는 실용적인 제어 수단을 제공하지 않습니다.- @u/obi1kenobi82 — 맞습니다. 블로그 글도 같은 점을 말합니다. 이 사례는 사용자가 기본 설정에서 안전한 선택을 하게 만드는 문제입니다. 대체로 기본값인
actions/cache와cargo-miri조합이 자격 증명과 함께 쓰일 때 기본적으로 문제가 생겼습니다. Rustacean으로서 아직 개선할 수 있는 부분을 개선하는 일입니다. 다른 문제가 남아 있더라도 마찬가지입니다. - @u/briansmith — 반대로
cargo-miri가 모든 환경 변수를 저장한 데는 이유가 있습니다. 어떤 변수가 빌드에 영향을 주는지 불분명하기 때문입니다. 이제 저장하는 하위 집합이 많은 경우에 충분하지 않습니다.cc-rs가 그 예입니다. 동시에 악성 PR은build.rs를 추가해 모든 환경 변수를 쓸 수 있고, 덜 눈에 띄는 방법으로도 같은 일을 할 수 있습니다. 따라서 보안이 실제로 개선됐는지는 분명하지 않은데, 동작은 이전보다 덜 정확해진 것처럼 보입니다. 사용자는 시크릿 관리부터 고쳐야 합니다. - @u/obi1kenobi82 — 악성 PR이 모든 환경 변수를 쓸 수 있다는 점은 문제의 본질이 아닙니다. 문제는 다른 job이 공유하는 GitHub Actions 캐시에서 그 환경 변수를 읽을 수 있다는 점입니다.
- @u/briansmith — 블로그 글의 PR은 환경 변수를 읽는 변경이 아니라 쓰는 변경 아니었나요? 악성 PR을 이야기한다면, job step에 노출된 민감한 환경 변수가 캐시에 기록되지 않게 막는 일은 현실적으로 어렵다는 뜻입니다. 애초에 job이 접근할 수 있는 시크릿이 없도록 만드는 데 집중하는 편이 훨씬 낫습니다. GitHub Actions에서는 이 설정에 이례적이고 예상하기 어려운 작업이 필요합니다.
- @u/obi1kenobi82 — 진단에는 동의하지만 제안한 대응에는 동의하지 않습니다. 둘 중 하나만 고를 필요는 없습니다. 안전한 기본값을 제공하고,
cargo-miri가 추가로 저장할 값을 opt-in 인자로 받게 만들면서, job에 시크릿을 넣지 않거나 줄이는 작업도 함께 할 수 있습니다. - @u/berrita000 — fork에서 온 PR의 GitHub Actions job은 시크릿에 접근하지 못합니다. 하지만 저장소의 main 쪽 job이 만든 캐시에는 접근할 수 있습니다.
- @u/obi1kenobi82 — 맞습니다. 블로그 글도 같은 점을 말합니다. 이 사례는 사용자가 기본 설정에서 안전한 선택을 하게 만드는 문제입니다. 대체로 기본값인
- @u/insanitybit2 — 회사의 Rust 코드에서 사용하는 방식을 강력히 권합니다. 모든 workflow를 명시적으로 ‘코드를 실행하는 workflow’와 ‘시크릿을 가진 workflow’로 나눕니다. 예를 들어
cargo build에는permissions: {}를 지정하고, shell script가 artifact를 권한이 있는 위치로 업로드하게 합니다. Miri도 실행하지만 모든 테스트와 마찬가지로permissions: {}상태에서 실행하고 artifact만 만들 수 있게 합니다. 이 방식은 supply-chain attack을 크게 줄입니다. 완벽하지는 않습니다. 캐시 오염 문제가 있고 artifact도 오염될 수 있지만, 위험을 크게 낮춥니다.- @u/obi1kenobi82 — 좋은 방식입니다. 저도
cargo-semver-checks와 그 의존성 저장소를 포함해 제 저장소에 적용하기 시작했습니다. 설정이 조금 번거롭지만 위험을 줄이는 효과가 크다는 데 동의합니다.
- @u/obi1kenobi82 — 좋은 방식입니다. 저도
- @u/obi1kenobi82 — RustConf에서 저를 만난 분들은 알겠지만, 지난 몇 주 동안 생태계 보안 작업을 꽤 많이 했습니다. 이번 문제는 GPT-6 Astra의 도움으로 발견했습니다. 숙련된 사람이 사용하면 버그, unsoundness, 보안 취약점을 찾는 데 매우 강력한 도구입니다. 공격자가 이미 침입한 뒤 수습하는 일은 충분히 어렵습니다. 생태계 차원에서 이런 문제를 미리 찾고 고치는 일을 돕고 싶습니다. 동의한다면 라이브러리를 스캔해 발견 사항을 보내드릴 수 있습니다. 이번 Miri와 GitHub Actions 문제를
[email protected]에 제보한 방식과 같습니다. 과정이 궁금하면 질문해도 됩니다. - @u/bzbub — 이런 종류의 ‘cache poisoning’ 스택을 익히고, 제3자 PR의 권한을 크게 제한하거나 아예 끄는 방식을 고려할 가치가 있습니다. CI 규칙을 확인할 때
zizmor도 사용하세요. https://github.com/zizmorcore/zizmor 와 https://safedep.io/tanstack-github-actions-cache-poisoning/ 를 참고할 만합니다. 두 번째 링크는 JavaScript 생태계의 TanStack에서 발생한 사례입니다. - @u/dutch_connection_uk — 정확히 이해했는지 확인하고 싶습니다. 이 문제로 유출되는 시크릿은 builder나 다른 DevOps 작업에 쓰는 시크릿인가요? 애플리케이션과 직접 관련된 시크릿은 아닌가요? 처음 읽었을 때는 사람들이 시크릿을 프로그램 안에 직접 넣는다면 그게 더 큰 문제라고 생각해서 다시 읽었습니다. ‘GitHub Actions가 시크릿을 유출한다’는 제목이었으면 이런 혼동이 조금 줄었을 것 같습니다.
- @u/numberwitch — Miri가 대체 뭔가요?
- @u/obi1kenobi82 — 무엇보다
unsafe사용이 올바른지 확인하는 도구입니다.
- @u/obi1kenobi82 — 무엇보다
원문: Rust Blog / 번역·요약: Trawling