[번역] 패키지 보안의 남은 과제들
Soshy·

2026년 5월, Nx의 한 기여자가 게시된 지 77분밖에 되지 않은 악성 의존성을 설치했다. 저장소에는 7일의 쿨다운 기간이 설정되어 있었지만, 프로젝트에 고정된 pnpm 버전은 이 설정을 지원하기 전 버전이었다. 클라이언트는 해당 검사를 건너뛰었고 패키지를 그대로 설치했다. 이때 탈취된 자격 증명은 일주일 뒤 악성 Nx Console 확장 프로그램이 배포되는 데 영향을 미쳤다.
최근 몇 달 사이 패키지 매니저의 기본 보안 설정은 계속 개선되고 있다. 새 릴리즈에 대한 쿨다운이나 설치 스크립트 실행 제한 같은 기능들이 대표적이다.
하지만 올해 내가 쓴 글들을 다시 살펴보면, 각 보안 장치가 개별적으로는 잘 동작해도 그 사이의 빈틈을 메우는 일은 여전히 사용자에게 남아 있다는 점을 계속 마주하게 된다.
설정한 정책이 실제로 자신이 사용하는 모든 도구에서 적용되는지 확인해야 하고, 스캐너가 취약점을 발견했을 때는 수정 버전을 릴리즈할 사람까지 찾아야 한다.
설치 권한
Nx는 이미 2025년 8월에도 다른 형태의 침해 사고를 겪었다.
당시 npm의 post-install 스크립트가 자격 증명을 탈취해 공개 GitHub 저장소에 업로드했다. 이 스크립트는 개발자의 권한으로 실행됐기 때문에 해당 패키지와 무관한 자격 증명에도 접근할 수 있었고 네트워크도 사용할 수 있었다.
둘 다 별도의 승인이 필요하지 않았다.
pip의 빌드 시스템 인터페이스는 의존성 메타데이터를 얻기 위해 백엔드를 실행할 수 있다. 메타데이터 훅이 없다면 wheel을 빌드할 수도 있다.
pip의 격리된 빌드 환경은 Python 의존성을 분리해 주지만, 그 안에서 실행되는 백엔드 자체는 pip를 실행한 사용자의 OS 권한으로 동작한다.
Composer의 allow-plugins 설정은 기본적으로 모든 플러그인을 차단한다. 대화형 환경에서는 새로운 플러그인을 활성화하기 전에 사용자에게 묻는다.
하지만 설치 과정에서는 어떤 코드를 실행할지뿐만 아니라, 허용된 코드가 무엇을 할 수 있는지도 제어할 필요가 있다.
예를 들어 컴파일러에는 빌드 결과물을 쓸 권한이 필요할 수 있지만, 개발자의 클라우드 자격 증명까지 읽을 필요는 없다.
빌드를 허용한다는 것은 해당 단계에 필요한 권한만 허용한다는 의미여야 한다.
릴리즈 승격
2023년 12월 Ledger Connect Kit 공격에서는 런타임에 CDN에서 최신 라이브러리를 가져오는 로더를 통해 악성 릴리즈가 애플리케이션까지 전달됐다.
하위 애플리케이션 개발자는 악성 버전을 받기 위해 애플리케이션을 다시 빌드할 필요도, 업데이트를 따로 승인할 필요도 없었다.
Debian의 testing 승격 과정에서는 패키지가 성공적으로 빌드되는지, 의존성에 문제가 없는지, 릴리즈를 막을 수준의 버그가 없는지 등의 조건을 충족할 때까지 unstable에 머무르게 한다.
필요한 대기 시간은 긴급도에 따라 달라진다.
최근 여러 언어의 패키지 매니저에 도입되고 있는 쿨다운 기능 역시 패키지 게시 시각과, 해당 설정을 실제로 강제하는 클라이언트에 의존한다.
동시에 사용자는 긴급한 수정 사항을 빠르게 받아들일 수 있는 검토된 예외 경로도 필요하다.
나는 각 프로젝트가 타이머 예외 처리 방식을 제각각 만들기보다는, 이런 검토 절차 자체가 공동 서비스로 운영되고 자금 지원을 받는 편이 낫다고 생각한다.
빌드 의존성
2025년 3월 tj-actions/changed-files 침해 사고 이전에는 SpotBugs, reviewdog, tj-actions/eslint-changed-files가 연이어 침해되는 과정이 있었다.
자격 증명과 GitHub Actions 의존성이 이 프로젝트들을 서로 연결했고, 마지막 공격에서는 워크플로 로그에 비밀 정보가 노출됐다.
이러한 관계는 애플리케이션의 package lockfile 안에는 존재하지 않았지만, 애플리케이션을 빌드하는 인프라에는 분명 영향을 미쳤다.
2024년 12월 Ultralytics 공격의 첫 번째 단계에서는 GitHub Actions 캐시 포이즈닝을 통해 정상적인 릴리즈 워크플로에 악성 데이터가 주입됐다.
이후 정상적으로 승인된 릴리즈 워크플로가 감염된 패키지를 직접 빌드하고 배포했다.
이 경우 저장된 레지스트리 토큰을 없앤다고 해도 공격 경로는 사라지지 않는다. 이미 침해된 빌드 자체가 패키지를 배포할 권한을 가지고 있었기 때문이다.
따라서 GitHub Actions 의존성 목록에는 각 Action이 다운로드하는 도구뿐만 아니라, 해당 Action에 부여된 권한까지 포함되어야 한다.
최상위 Action의 버전을 고정하는 것만으로는 충분하지 않다.
그 Action이 변경 가능한 스크립트를 내려받거나, 신뢰할 수 없는 작업이 쓸 수 있는 캐시에서 실행 가능한 파일을 복원한다면 여전히 변경된 코드가 실행될 수 있다.
소스와 아티팩트
초기 xz 취약점 공개에서는 Git 저장소에는 존재하지 않고 릴리즈 tarball에만 포함된 악성 빌드 로직이 설명됐다.
페이로드에 사용된 일부 데이터는 저장소의 테스트 fixture 안에도 숨어 있었다.
즉, Git에서 태그가 붙은 소스를 체크아웃해 리뷰하는 것만으로는 tarball에만 포함된 악성 부분을 놓칠 수 있었다.
Ultralytics 공격의 첫 번째 악성 릴리즈들은 실제 빌드 워크플로에서 생성된 attestation을 가지고 있었다.
이는 조사 과정에서 침해가 어느 단계에서 발생했는지 파악하는 데 도움이 됐다.
이후 공격에서는 오래된 PyPI API 토큰을 사용했고 attestation은 존재하지 않았다.
두 방식 모두 악성 패키지를 만들어냈지만, attestation 덕분에 어떤 경로로 패키지가 배포됐는지를 구분할 수 있었다.
Go의 reproducible toolchain 작업은 배포된 바이너리를 독립적으로 다시 빌드한 결과와 비교한다.
서명은 인프라가 침해되더라도 누가 무엇을 승인했는지에 대한 증거를 남길 수 있다.
하지만 이런 검사가 소스 코드 리뷰까지 대신해 주는 것은 아니다.
provenance 배지는 아티팩트가 어떤 방식으로 만들어졌는지를 기록한다.
하지만 입력값을 승인하는 것과 결과 코드를 리뷰하는 것은 서로 다른 문제다.
이름과 배포자
2018년 event-stream 사건에서는 새로운 maintainer가 flatmap-stream을 의존성으로 추가했고, 이후 해당 의존성의 악성 릴리즈가 Copay 지갑의 빌드를 공격했다.
사용자 입장에서는 계속 익숙한 event-stream이라는 패키지 이름을 사용하고 있었지만, 실제로 패키지 내용을 변경할 수 있는 사람은 이미 바뀌어 있었다.
2022년 ctx 탈취 사건은 만료된 이메일 도메인을 이용했다.
공격자가 해당 도메인을 등록한 뒤 PyPI 계정의 비밀번호를 재설정했고, 얼마 지나지 않아 악성 패키지를 업로드했다.
2023년 Packagist 계정 탈취에서는 재사용된 비밀번호가 사용됐다.
공격자는 14개 패키지의 소스 저장소를 다른 위치로 리디렉션했다.
Packagist는 패키지 설명이 변경된 것은 확인했지만 실제 악성 코드가 배포된 정황은 없다고 밝혔다.
이 사건에서 실제로 확인된 문제는 소스 저장소 자체가 다른 곳으로 바뀔 수 있었다는 것이다.
사용자는 패키지의 배포자나 소스 저장소가 변경될 때 다시 리뷰하도록 정책을 설정할 수 있다.
그러기 위해서는 레지스트리가 인증 정보뿐 아니라, 패키지를 변경할 권한이 누구에게서 누구에게로 넘어갔는지에 대한 메타데이터도 제공해야 한다.
코딩 에이전트
jqwik의 anti-AI 조항은 테스트 출력 안에 코딩 에이전트를 대상으로 한 지시사항을 넣는다.
에이전트에게 이전 지시와 테스트 결과를 무시하라고 말한다.
이 기능은 maintainer가 의도적으로 추가한 것이다. 문서에서는 실제 에이전트가 이 지시를 따를지 여부까지는 다루지 않는다.
Johann Rehberger의 module shadowing 시연에서는 코딩 에이전트가 압축 파일에서 추출된 악성 struct.py 옆에 Python helper 스크립트를 작성했다.
helper의 import 문은 공격자가 만든 모듈을 불러왔고, 동시에 사용자가 요청한 디코딩 결과도 정상적으로 만들어냈다.
에이전트는 압축 파일에 포함된 수상한 실행 파일을 직접 실행하는 것은 거부했지만, 자신이 생성한 helper 코드가 또 다른 실행 경로를 만들어버렸다.
따라서 에이전트가 생성한 helper 프로그램은 제한된 환경에서 실행되어야 하며, import 경로 역시 통제되어야 한다.
모델에게 실행할 명령어를 꼼꼼하게 검토하라고 시키는 것만으로는 부족하다.
실제 명령이 실행되는 순간 인터프리터가 파일과 자격 증명에 접근할 수 있다면 위험은 그대로 남아 있다.
반복되는 버그
Composer는 2021년 저장소 URL을 통한 command injection 취약점을 공개했고, 2022년에는 branch 이름을 이용한 또 다른 취약점을 공개했다.
두 취약점 모두 외부에서 들어온 값이 버전 관리 시스템 명령의 옵션으로 전달되는 문제였다.
URL 처리 문제를 수정했지만, 다른 입력값과 다른 명령 호출 경로까지 모두 점검된 것은 아니었다.
Packagist에 따르면 두 취약점 모두 실제 악용 사례는 알려지지 않았다.
pip의 2023년 Mercurial 보안 권고에서는 조작된 revision 값으로 hg clone에 설정값을 주입할 수 있었다.
Go의 2026년 1월 VCS 보안 권고에서도 조작된 버전 문자열이 외부 명령으로 전달됐으며, Mercurial과 Git에서 서로 다른 결과가 발생했다.
패키지 매니저가 새로운 VCS 백엔드를 추가한다면, 기존에 알려진 악성 URL과 revision 모음을 해당 백엔드에 그대로 실행해 볼 수 있어야 한다.
내가 정리한 인자 처리 관련 조사와 패키지 매니저 버그 목록에는 이미 이런 regression test에 활용할 수 있는 공개 사례가 모여 있다.
Maintainer 승계
내가 5월에 진행한 의존성 조사에서는 5,874개 저장소 중 713개가 해당 조사 기준에서 사실상 죽은 프로젝트로 분류됐다.
그중 391개는 archive 처리조차 되어 있지 않았다.
즉, GitHub의 archived 여부만 확인했다면 절반이 넘는 비활성 프로젝트를 놓쳤을 것이다.
같은 데이터를 다른 관점에서 보면, 죽었거나 장기간 활동이 없는 패키지 중 레지스트리 배포자가 단 한 명뿐인 패키지가 1,414개였다.
Jean Boussier가 Ruby 의존성 유지보수에 대해 작성한 글에는 httpclient 사례가 나온다.
이 패키지는 2016년부터 2025년까지 새로운 릴리즈가 없었고, 함께 번들된 CA 인증서는 2023년에 만료됐다.
사용자들은 자신이 사용하는 사본에 직접 패치를 적용했고, 애플리케이션은 깨졌다.
문제가 무엇인지, 어떻게 고쳐야 하는지 모두 알고 있었지만 새 버전을 릴리즈하려면 결국 누군가가 실제 배포 권한을 확보해야 했다.
사용자에게는 현실적으로 사용할 수 있는 패치와 포크 경로가 필요하다.
동시에 레지스트리는 앞에서 살펴본 악의적인 maintainer 교체 공격에도 견딜 수 있는 승계 절차를 마련해야 한다.
취약점 대응 예산에는 패치를 downstream 애플리케이션에 적용해 테스트하고, 대체 패키지를 유지보수할 사람의 비용까지 포함되어야 한다.
리포트 분류
ISC는 2026년 초 약 3개월 동안 하나의 신고 플랫폼을 통해 총 150건의 제보를 받았다고 밝혔다.
그중 8건은 유효했고, 8건은 아직 조사 중이었으며, 134건은 오탐이었다.
이 수치는 하나의 신고 채널에 대한 것이며 AI를 활용한 모든 보안 연구를 대표하는 수치는 아니다.
내가 5월 curl을 대상으로 스캐너를 실행했을 때, 한 스캐너는 15만 줄짜리 설정 파일을 생성했다.
이 파일은 form parsing 과정에서 stack overflow를 발생시켰다.
하지만 해당 버그가 발생하려면 공격자가 명령줄 인자나 설정 파일을 직접 제공할 수 있어야 했다.
curl의 보안 공개 정책은 이러한 입력을 보안 경계 밖으로 본다.
스캐너 역시 해당 정책을 적용해, 공개 전에 이를 보안 취약점이 아닌 품질 버그로 분류했다.
Django의 보안 신고 가이드는 실제로 동작하는 proof of concept과 테스트한 버전을 요구한다.
반면 CVSS 점수나 긴 배경 설명은 권장하지 않는다.
나 역시 Scrutineer의 워크플로를 비슷한 방식으로 설계했다.
신고가 전달되기 전에 검증과 사람의 리뷰가 이루어지고, 수정 사항이 실제 릴리즈에 포함될 때까지 추적한다.
내가 보안 스캐너에 붙이고 싶은 지표는 maintainer에게 요구한 조사 시간 대비, 실제 사용자에게 전달된 검증된 수정 작업이 얼마나 되는가다.
의존성 측정
Daniel Stenberg는 3월 LF Insights가 curl의 연간 다운로드 수를 10,467회로 집계하고 있는 것을 발견했다.
하지만 curl 공식 사이트에서만 한 달에 약 25만 건의 소스 tarball 다운로드가 발생했다.
여기에는 Linux 배포판, 컨테이너, 다른 소프트웨어에 내장된 사본도 포함되지 않는다.
2015년 Open Source Census에서는 xz가 254위였으며 위험도 점수는 13점 만점에 6점이었다.
리뷰어가 xz의 중요성을 별도로 표시했음에도 그랬다.
해당 평가에서는 기여자 수가 최대 5점까지 영향을 미쳤다.
따라서 활동 중인 기여자 한 명만 추가되어도 겉보기 유지보수 상태는 좋아질 수 있지만, 점수 계산만으로는 그 사람의 의도가 무엇인지 전혀 알 수 없다.
sudo와 polkit은 후보 목록 자체에 포함되지 않았다.
즉, 랭킹 공식을 아무리 개선해도 이 프로젝트들은 평가 대상에 들어오지 않는다.
어떤 wrapper 패키지의 레지스트리 다운로드 수와 Linux 배포판에서의 설치 횟수는 서로 다른 것을 측정한다.
다른 프로그램 안에 번들된 사본은 어느 통계에도 잡히지 않을 수 있다.
따라서 보안 예산을 배분할 때는 사용량을 제대로 측정하기 어려운 의존성도 고려해야 한다.
star 수나 commit 수보다 실제 유지보수 역량을 보여주는 근거 역시 함께 살펴봐야 한다.
보안 권고 매칭
Ruby의 이슈 트래커에는 별도 gem에서는 이미 CVE-2024-35176이 수정됐음에도 Ruby 3.3.2가 REXML 3.2.6을 포함해 배포된 사례가 기록되어 있다.
Microsoft의 System.Text.Json 보안 권고 역시 하나의 라이브러리가 독립 패키지 형태와 런타임 내장 형태로 동시에 배포되는 상황을 다룬다.
배포 방식이 다르면 업데이트 방법도 달라진다.
따라서 의존성 인벤토리는 이러한 사본을 구분하고, 실제 애플리케이션에서 어떤 사본이 로드되는지까지 확인해야 한다.
Homebrew 취약점 통합 작업에서는 809개 formula에 걸쳐 1,191개의 패치를 찾아냈고, 각 패치가 어떤 보안 권고를 해결하는지 기록하는 메타데이터를 추가했다.
스캐너가 upstream 버전만 비교했다면 이미 패치된 빌드까지 취약하다고 잘못 판단했을 것이다.
기존 스캐너에는 또 다른 문제도 있었다.
예전에 설치된 keg를 검사하면서 현재 시점의 formula를 읽고 있었다.
이를 설치된 keg의 SBOM을 읽도록 수정하자 실제 설치된 버전을 정확하게 평가할 수 있었다.
7월 Homebrew의 glibc 패치 세트에서는 또 다른 한계가 드러났다.
이 패치 세트는 CVE 10개를 해결했지만, 소스 저장소를 기준으로 OSV에 질의하면 하나만 반환됐다.
나머지 9개의 취약점 정보도 데이터베이스에는 존재했지만 affected-package 메타데이터가 없었다.
즉, 데이터베이스에 정보가 있어도 질의 방식에 따라 결과에서 누락될 수 있다.
보안 책임
ENISA가 3월 발표한 패키지 매니저 보안 권고에서는 의존성을 선택할 때 star 수, 다운로드 수, 최근 commit 등을 고려할 것을 권장한다.
다만 인기도만으로 판단해서는 안 된다고 명시적으로 경고한다.
이 문서의 범위에서는 OS 패키지 매니저, 안전한 패키지 배포 과정, 그리고 패키지 매니저 자체의 배포 채널은 제외된다.
따라서 사용자가 해당 가이드를 모두 따른다 해도 설치 과정의 이런 부분을 다루는 별도 정책은 여전히 필요하다.
SPDX의 supplier 필드는 패키지를 누가 제공했는지 나타내며 NOASSERTION 값도 허용한다.
하지만 이 필드가 있다고 해서 지원 계약이 존재하는 것은 아니다.
이 필드를 근거로 자원봉사 maintainer가 기업의 incident response까지 책임져야 한다고 해석한다면, 인벤토리 어디에도 정의되지 않은 의무를 추가하는 셈이다.
빌드 attestation과 조직의 선언 역시 서로 다른 질문에 답한다.
조달 과정에서는 두 정보를 같은 항목 아래 묶을 수 있지만, 그렇다고 두 정보의 의미까지 같아지는 것은 아니다.
Æva Black이 FOSDEM에 제안한 funded attestations에서는 소프트웨어를 사용하는 제조사가 그 비용을 부담하고, 해당 지출을 upstream 유지보수와 연결한다.
동시에 maintainer에게 책임을 전가하지는 않는다.
기업이 특정 의존성의 빌드 방식에 대한 증명이나 지속적인 유지보수 약속을 요구한다면, 그 증거를 만드는 작업과 유지보수를 제공하는 비용 역시 해당 기업의 조달 예산에서 부담해야 한다.