공급망 공격, crates.io 비판, 그리고 Rust 생태계에서 보안 책임이 실제로 어디에 있는지에 대한 글.
2026년 4월 11일Lobsters
혹시 모르셨다면, 저는 개발자가 아닙니다. 저는 사실 비최적의 컴퓨팅 파워 사용에 짜증이 난 자폐성향의 캣걸이고, 그걸 고치다 보니 우연히 프로그래밍을 하게 되었을 뿐입니다. 중요한 점은, 여기에는 막후에서 기반 기술에 대해 사람들과 이야기하는 일도 포함된다는 것이고, 그 덕분인지 저는 이 분야의 사회적 측면을 더 잘 의식하게 된 것 같습니다.
그래서 저는 공급망 공격에 대한 crates.io 비판에 대해 의견 이 있습니다. 비슷한 글을 열몇 개쯤 보고 나니, 왜 그 비판이 핵심을 벗어나는지에 대해 몇 마디 해야겠다는 생각이 들었습니다.
본론에 들어가기 전에, 우선 공급망 공격이 애초에 어떻게 일어나는지, 그리고 이를 고치기 위한 몇 가지 흔한 아이디어가 왜 잘 통하지 않는지 이야기해 봅시다.
악성 의존성이 프로젝트에 추가되는 이유는 여러 가지입니다. 그중 가장 덜 교묘한 경우는 오타 스쿼팅입니다. 이는 악성 라이브러리가 실제 라이브러리와 비슷한 이름을 가질 때 발생합니다. 예를 들어 num_cpu 대 num_cpus 같은 식입니다. 흔히 거론되는 해결책으로는 직접 URL을 쓰거나 네임스페이싱을 도입하는 방법이 있습니다.
그럼 그게 도움이 되는지 봅시다. 누군가 Cargo.toml에 다음 줄들을 추가하는 PR을 보냈다고 해 봅시다:
[dependencies]
bitflags = { git = "https://github.com/bitflags/bitflags" }
itertools = { git = "https://github.com/itertools/itertools" }
rand_core = { git = "https://github.com/rust-random/rand_core" }
이 URL들 중 하나는 가짜입니다. 어느 것인지 알 수 있나요? itertools입니다. 올바른 URL은 https://github.com/rust-itertools/itertools입니다. https://github.com/itertools는 아무 계정이나 하나일 뿐입니다. 참고로 https://github.com/rust-bitflags는 아예 등록조차 되어 있지 않습니다.
자신이 사용하는 각 패키지의 URL을 모두 기억할 수 있다고 생각한다면, 아마 틀렸을 겁니다. 많은 크레이트가 개인이 아니라 GitHub 조직에서 관리되기 때문에, dtolnay와 BurntSushi는 아마 믿을 수 있다는 정도를 기억하는 것만으로도 충분하지 않습니다. 하지만 이것조차 충분히 보수적이지 않습니다. https://gitlab.com/BurntSushi는 비어 있고 https://glthub.com은 판매 중이니, 공격자에게는 다른 선택지도 많습니다.
crates.io 내부 네임스페이싱이든, GitHub 조직이든, 도메인이든, 크레이트 식별자를 더 길게 만들수록 사용자가 그것을 정확히 기억하기는 더 어려워지고, 따라서 오타 스쿼팅을 알아차리기도 더 어려워집니다.
Rust는 빌드 스크립트와 프로시저 매크로에 당신의 PC에 대한 완전한 접근 권한을 줍니다. 더 나쁜 점은, rust-analyzer가 프로젝트 디렉터리를 열 때 cargo check를 실행하므로, 사실상 0클릭 원격 코드 실행이 될 수 있다는 것입니다.
몇몇 사람들은 이 문제를 해결하려고 했습니다. build.rs 샌드박싱에 대해서는 열려 있는 이슈가 있고, 프로시저 매크로를 WebAssembly로 컴파일하려는 몇 가지 실험도 있었습니다.
하지만 이건 거의 실현 가능하지 않습니다. cargo build는 안전해질 수 있을지 몰라도, 대개 그 직후에 cargo test나 cargo run을 실행하게 되는데, 이건 샌드박스 처리할 수 없습니다. Rust 개발을 안전하게 만들려면 빌드 시점만이 아니라 그 이상이 필요하고, 이는 cargo만으로 책임질 수 없는 강력한 시스템 수준 격리를 요구합니다.
자주 제기되는 문제 중 하나는 crates.io의 코드와 Git의 코드가 항상 일치하지는 않는다는 것입니다.
우선, 이 문제는 해결이 간단하지 않습니다. crates.io를 단순히 크레이트 이름을 저장소 URL에 매핑하는 DNS처럼 바꿀 수는 없습니다. crates.io는 설계상 크레이트 유지보수자가 무언가를 삭제함으로써 하위 소비자를 망가뜨릴 수 없도록 하고 있기 때문입니다:
crates.io의 주요 목표 중 하나는 시간이 지나도 변하지 않는 크레이트의 영구 아카이브 역할을 하는 것이며, 버전 삭제를 허용하는 것은 이 목표에 어긋난다.
이 제한은 아마도 인기 라이브러리가 npm에서 삭제되어 CI 빌드가 깨졌던 left-pad 사건 때문에 설정되었을 가능성이 큽니다. npm은 중앙집중형이기 때문에 이를 빠르게 고칠 수 있었습니다. 얇은 crates.io는 그럴 가망이 없으니, 복사본을 저장하고 제공하는 것입니다.
여전히 crates.io가 cargo publish 시점에 저장소에서 파일을 가져오게 할 수는 있습니다. 하지만 유지보수자가 그 뒤에 강제 푸시를 해버릴 수 있다면, 그건 좋은 보안 메커니즘이 아닙니다.
어쩌면 crates.io가 주기적으로 저장소를 스캔해서 히스토리 변경을 찾을 수 있을지도 모릅니다. 하지만 정확히 그게 무슨 뜻일까요? 릴리스 커밋을 master에서는 지우되 태그에는 남겨 두면 그것도 해당될까요? 제가 커스텀 포지에 저장소를 호스팅하면서 crates.ioUser-Agent에는 한 가지 히스토리를, 나머지 사람들에게는 다른 히스토리를 보여 준다면요?
아니면 Git과 crates.io에 서로 다른 코드가 있어야 하는 정당한 이유가 있을 수도 있습니다. 크레이트에 자동 생성 코드가 들어 있다면, 아마 릴리스 시 CI에서 이를 생성하는 편이 나을 것입니다. 설치할 때마다 build.rs에서 값비싼 코드 생성을 돌리고 싶지는 않겠죠?
어느 선택지든 단점이 있습니다. 기존 패키지를 깨뜨릴 수도 있고, 선의의 히스토리 재작성에 대해 오탐을 낼 수도 있습니다. 그래도 저는 cargo audit가 저장소를 스캔했으면 좋겠지만, 그것이 강제적인 한계가 될 수는 없고, 그렇다는 것은 결국 우회 가능하도록 설계될 수밖에 없다는 뜻입니다.
이 모든 문제에는 명시적으로 인정되지 않은 공통 가정이 하나 있습니다. crates.io에서 악성 코드를 막아내는 것이 “Rust”의 책임이라는 가정입니다. 당신이 어떤 의존성을 쓰기로 결정했고, 그다음 cargo add totally-safe-package가 자격 증명을 훔쳐 갔다면, 그것이 crates.io의 본질적 잘못이라는 식입니다. 하지만 Rust가 어떻게 개발되는지 생각해 보면, 이건 정말로 엇나간 주장입니다.
여러분 중 많은 분들이 오픈소스 소프트웨어를 사용하고, MIT 라이선스를 기억하고 계실 거라 확신합니다:
이 소프트웨어는 “있는 그대로” 제공되며, 어떠한 종류의 보증도 없고, […], 특정 목적에의 적합성 및 비침해성에 대한 보증도 없다. […]
이 말은 Rust에도 똑같이 적용됩니다. Rust는 인기가 높음에도 불구하고, GitHub나 Linux가 그런 의미에서 Microsoft나 RedHat의 후원을 받는 것처럼 대기업의 후원을 받는 프로젝트가 아닙니다. 컴파일러와 std는 주로 자원봉사자들이 개발하며, 그들은 커뮤니티의 다른 구성원으로부터 받는 드문 기부 외에는 여기서 얻는 것이 없습니다.
저는 이 구조가 어쨌든 작동한다는 것 자체가 놀랍습니다. 아마 너무 믿기 어려워서, 사람들이 Rust Foundation 같은 곳에 숨은 고용 구조라도 있다고 생각하는 것일지 모릅니다. 하지만 실제로는 보조금을 제외하면 2024년에 단 네 명의 소프트웨어 엔지니어에게만 급여를 지급했습니다.
그래서 누군가 공급망 공격의 해결책을 안다고 말할 때, 제게는 정말 황당하게 느껴집니다. 예를 들면 다음 같은 것들이죠. (정답에 밑줄을 치세요):
사회적 문제를 기술로 헛되이 고치려 드는 것은 모든 개발자의 특기이지만, 잠깐만 현실적으로 생각해 봅시다. 문제는 인력이 부족하다는 것이지, 아직 아무도 블록체인 기반 Web of Trust용 멋진 GUI를 만들지 않았다는 게 아닙니다. 뭐 그런 식의 문제가 아니라는 뜻입니다.
우리는 중앙집중형 레지스트리가 제공할 수 있는 수준의 보안에 근처에도 가지 못하고 있습니다. 소프트웨어 측면에서 Rust 팀은 2025년에 오타 스쿼팅 탐지, 동적 빌드 스크립트 분석, 실시간 코드 스캐닝을 위한 도구를 만들었거나 시범 운영했습니다. 인적 측면에서는 Rust Foundation이 2025년에 온콜 엔지니어를 채용했고, 2026년에는 두 번째 인프라 엔지니어를 채용했습니다. 이게 늦었다고 느껴진다면, 뭐, 그들은 2023년에 순손실을 냈습니다. 소프트웨어는 싸지 않으니까요.
Ubuntu의 것처럼 작고, 사전 중재되고, 안정 릴리스 중심이며, 사적 자금으로 운영되는 레지스트리와, 크고, 사후 중재되고, 롤링 릴리스이며, 대부분 자원봉사에 의존하는 레지스트리에서 같은 수준의 보안을 기대할 수는 없습니다. 규모가 비교가 안 되며, 이미 과로 중인 팀의 일을 더 복잡하게 만들면 문제가 해결될 거라고 제안하는 것은 그저 모욕적일 뿐입니다.
제가 말하고 싶은 것은, 당신이 사용하는 크레이트를 감사할 책임은 당신에게 있다는 것입니다. 완벽한 세상이라면 그렇지 않을지도 모르지만, 우리가 사는 이 자본주의적 지옥도에서는 이에 대해 책임을 져야 한다고 주장할 수 있는 다른 누구도 없고, 그러면 남는 것은 크레이트 사용자, 즉 당신입니다.
그리고 Rust는 이를 위한 도구를 모두 제공합니다.
락파일은 설치를 검증된 버전으로 제한합니다. cargo add <dep>@<version>은 특정 버전의 크레이트를 설치합니다. cargo-vet도 유용할 수 있습니다. crates.io는 90일 다운로드 그래프를 보여 주는데, 이는 악성 크레이트가 흉내 내기 어렵고, 이메일을 통한 멀웨어 신고에도 대응합니다. 조금 모험을 해보고 싶다면, 크레이트 사이드바의 “browse source”를 눌러 제출된 크레이트 소스를 확인할 수 있습니다. 간단히 훑어보는 것만으로도 대부분의 단순한 공격은 걸러낼 수 있습니다. cargo update --dry-run은 실제로 의존성을 업데이트하지 않고도 오래된 크레이트 목록을 보여 주므로, 무엇을 다시 확인해야 하는지 알 수 있습니다.
샌드박싱의 경우, Nix 격리를 제외하면 cargo-chef가 빌드를 샌드박스 처리하는 가장 직관적인 방법입니다. rust-analyzer와 cargo run까지 지원하는 일은 더 까다롭지만, 대개는 그냥 firejail을 쓸 수 있을 거라고 생각합니다. 누군가 이를 위한 스크립트를 공유해 준다면 보너스 점수를 주고 싶네요!
우리 중 보안 연구자는 거의 없지만, 공정하게 말하자면 그중 많은 부분은 상식과 약간의 호기심입니다. 그리고 당신이 코드를 감사하지 않는다면, 그럼 누가 하겠습니까?