Package managers have an integrity problem
패키지 매니저에는 무결성 문제가 있습니다
Trail of Bits가 소스 코드 의존성을 관리하는 패키지 매니저 25종을 시험한 결과, 기본 설정으로 내려받은 패키지의 무결성을 모두 확인한 도구는 두 종뿐이었습니다. 글은 lockfile 해시와 서명 검증의 한계를 비교하고, 개발자 신원 확인과 투명성 로그를 결합하는 방안을 제안합니다.
- 주제
에디터 노트
기본 설정에서 패키지 무결성을 확인하는 도구가 두 종뿐이라는 결과는 저장소 침해를 가정한 실험 조건 안에서 설득력이 있습니다. Docker와 mitmproxy로 응답을 직접 조작했고, 실제 설치와 악성 코드 실행 여부를 확인했기 때문입니다. 다만 한 버전과 플랫폼에서 대표 패키지 하나를 시험한 결과라 각 생태계 전체의 취약성을 수치로 일반화하지는 않습니다. 글은 lockfile에 해시를 넣는 것만으로는 충분하지 않다는 점을 구체적인 동작 차이로 보여줍니다. Pub처럼 불일치를 발견해도 설치를 계속하거나, 해시가 실제 내려받은 소스와 연결되지 않는 사례가 있습니다. 개발자 신원 서명과 투명성 로그를 결합하는 제안은 업데이트 정책과 무결성 확인을 분리한다는 점에서 글의 문제 진단과 맞닿아 있습니다. 다만 글도 검증 구현과 생태계 도입이 남은 과제라고 밝히며, 각 패키지 매니저에 적용할 세부 설계까지 제시하지는 않습니다.
AI 요약
패키지 저장소가 침해돼 정상 패키지에 악성 코드를 섞어 배포하면 패키지 매니저가 이를 찾아낼 수 있을까요? Trail of Bits는 소스 코드 의존성을 다루는 패키지 매니저 25종을 대상으로 저장소 응답을 조작하는 실험을 했습니다. lockfile이 있어도 변조를 기본 차단하는 도구는 절반이 안 됐고, 내려받은 모든 패키지의 무결성을 기본 검증한 도구는 Go와 vcpkg 두 종이었습니다.
저장소 변조를 재현한 실험
실험 환경은 Docker 컨테이너와 mitmproxy로 구성했습니다. 컨테이너 안에 프록시의 TLS 인증 기관을 설치해 패키지 저장소의 응답을 가로채고, 내려받는 패키지에 악성 코드를 삽입했습니다. lockfile을 포함한 경우와 제외한 경우를 모두 시험했으며, 기본 명령에서 악성 코드가 설치되고 샘플 코드 실행 중 동작하면 공격 성공으로 판정했습니다.
각 패키지 매니저의 최신 버전을 설치하고 빠른 시작 안내에 따라 프로젝트를 구성했지만, 결과는 패키지 매니저 한 버전과 한 플랫폼, 한 패키지를 대상으로 한 실험에 한정됩니다. PowerShell이나 CocoaPods의 Ubuntu 테스트처럼 일반적인 사용 환경과 다를 수 있는 구성도 있습니다. 따라서 모든 개발자가 취약하다는 뜻이 아니라, 합리적인 기본 사용 방식에서도 저장소 변조가 통할 수 있음을 보이는 시험입니다.
lockfile 해시는 조건에 따라 무결성을 보장합니다
Bun, npm, pnpm, Yarn, uv, Poetry, Bundler, Cargo, SwiftPM은 lockfile이 있을 때 해시를 확인하는 것으로 분류됐습니다. 다만 lockfile이 없거나 추가 설정이 필요한 경우도 많았습니다. Conda, Gradle, Pip, NuGet, Pub은 추가 단계가 필요했고, CocoaPods, Composer, Conan, cpanminus, Gem, Maven, PowerShellGet, PSResourceGet, R은 lockfile을 통한 검증을 하지 않거나 해당 기능이 없었습니다. Go와 vcpkg는 실험 설정에서 기본적으로 패키지 무결성을 검증했습니다.
lockfile에 해시가 적혀 있다고 항상 변조를 막는 것은 아닙니다. Pub은 다운로드한 파일의 SHA-256 값이 lockfile과 달라도 기본 동작에서는 오류를 알리고 설치를 계속합니다. 설치를 중단하려면 --enforce-lockfile을 지정해야 합니다. pnpm도 2026년 5월 11.4 버전 전까지는 기본 동작이 lockfile을 덮어쓸 수 있었고, --frozen-lockfile을 따로 지정해야 했습니다.
Conan의 lockfile에는 MD5 기반 recipe revision이 있지만, 글에서 설명한 용도는 암호학적 무결성 검증이 아니라 식별입니다. 패키지를 설치하면서 해당 해시를 다시 계산하지 않습니다. CocoaPods는 Podspec의 SHA-1 체크섬을 기록하지만 실제 소스 코드는 Git 저장소에서 받아오므로, 저장소가 변조된 같은 태그의 코드를 제공하면 감지하지 못할 수 있습니다. Composer의 lockfile도 Git 커밋 해시를 기록하지만, 내려받는 ZIP 파일 자체에서 Git 트리를 대조하지 않습니다.
lockfile은 의존성 버전을 고정하고 개발 환경을 공유하며 재현 가능한 빌드를 돕는 등 여러 목적으로 쓰입니다. 애플리케이션에서는 lockfile을 저장소에 포함하는 방식이 널리 권장되지만, 라이브러리에서는 의존성 충돌을 일찍 찾기 위해 제외하기도 합니다. 해시를 고정하면 새 버전의 보안 업데이트도 자동으로 따라오지 않으므로 lockfile을 갱신해야 합니다. 글은 무결성과 빠른 업데이트라는 요구가 현재의 해시 기반 방식에서 맞물리지 않는다고 짚습니다.
서명이 있어도 검증은 기본값이 아닙니다
시험 대상 가운데 서명을 기본 검증한 패키지 매니저는 Go뿐이었습니다. Bundler, Gem, Gradle, Maven, npm, NuGet은 설정을 추가해야 했고, 여러 도구는 검증 기능이 없거나 불완전했습니다. 저장소가 패키지를 서명하는 방식은 신뢰할 수 없는 미러나 일부 서버 침해에 대응하지만, 저장소 자체가 악성 패키지에 서명하면 막지 못합니다. 개발자 서명은 저장소가 업로드 뒤 패키지를 바꾸는 일을 막지만, 개발자가 키를 관리해야 합니다.
Sigstore의 keyless 서명은 개발자가 장기 비밀키를 관리하지 않고 OIDC 신원 확인을 거쳐 서명 인증서를 발급받는 방식입니다. CI/CD 작업이 저장소와 커밋 정보를 담은 인증서로 빌드 산출물에 서명하면 코드와 배포 파일의 연결을 확인할 수 있습니다. PyPI, npm Registry, RubyGems, Maven Central이 Sigstore 기반 증명을 지원하지만, 이를 검증하는 기능이 패키지 매니저에 기본으로 들어가 있지는 않습니다. 예를 들어 npm은 npm install에서 서명을 검사하지 않고 npm audit signatures를 별도로 실행해야 합니다. NuGet도 기본 서명 검증 모드가 accept라서, 사용자가 require를 설정하고 신뢰할 서명자를 지정해야 합니다.
Go의 체크섬 데이터베이스와 제안된 방향
Go는 패키지 내용의 해시를 체크섬 데이터베이스에 기록된 이름, 버전과 대조합니다. 데이터베이스 서명은 Go에 내장된 공개키로 검증합니다. 이 구조에서는 go.mod에 패키지별 해시를 저장하지 않아도 데이터베이스가 같은 패키지 버전의 내용을 고정합니다. 체크섬 데이터베이스는 변경 내역을 공개하는 투명성 로그의 사례이기도 합니다. 로그는 특정 사용자에게만 악성 패키지를 배포하는 공격을 드러내기 쉽게 하지만, 로그의 개인키가 침해되거나 로그를 감시하는 사람이 없으면 보호가 무너질 수 있습니다. 글은 Go 방식이 시험 대상 중 무결성 보호가 가장 강했다고 평가하면서도, 신뢰가 단일 개인키에 집중된다고 설명합니다.
글이 제안하는 방향은 lockfile에 특정 패키지 해시 대신 개발자 신원이나 공개키를 기록하는 것입니다. 버전을 고정하거나 업데이트를 허용하는 정책과 패키지 무결성을 분리하고, 투명성 로그를 더해 선택적 표적 공격도 확인하도록 설계할 수 있습니다. 이 아이디어는 pylock.toml의 증명 신원, Gradle 검증 메타데이터의 신뢰 키, Maven 플러그인의 PGP 키 목록 등에서 이미 일부 나타납니다. 다만 서명 검증 코드를 만들고 감사하는 일, 개발자와 생태계의 도입이 필요하다고 글은 덧붙입니다.
원문: Trail of Bits / 번역·요약: Trawling