의존성 쿨다운(최소 릴리스 경과 시간) 기능이 다양한 패키지 매니저와 업데이트 도구에 얼마나 빠르게 확산되었는지, 그리고 언어 생태계와 시스템 패키지 매니저의 차이 및 타임스탬프/기간 파싱 문제를 살펴본다.
package-managerssecurityecosystemsdeep-dive
Mar 4, 2026
이 글은 Seth Larson의 요청으로 작성했다. 그는 패키지 매니저 전반에 걸친 의존성 쿨다운을 분석해 줄 수 있냐고 물었다. 그의 문제 제기 방식은 이렇다. 모든 도구는 전역적으로 설정 가능한
exclude-newer-than=<relative duration>
를 지원해야 하며, 예를 들면
7d
처럼, 자율적 악용(자동화된 공격)에서의 대응 시간을 다시 인간이 개입할 수 있는 범위로 되돌려야 한다는 것이다.
공격자가 메인테이너의 자격 증명을 탈취하거나 방치된 패키지를 장악하면, 악성 버전을 배포한 뒤 자동화 도구가 아무도 눈치채기 전에 그것을 수천 개 프로젝트로 끌어오길 기다린다. William Woodruff는 2025년 11월에 dependency cooldowns의 필요성을 주장했고, 한 달 뒤 redux로 후속 글을 썼다. 요지는 간단하다. 패키지 버전이 레지스트리에 등록된 뒤 최소한의 기간이 지나기 전에는 설치하지 말자. 그러면 커뮤니티와 보안 벤더가 문제가 있는 배포를 표시할 시간을 확보할 수 있고, 그 전에 빌드가 그것을 끌어오는 일을 막을 수 있다. 그가 분석한 10건의 공급망 공격 중 8건은 기회의 창이 1주일 미만이었으므로, 7일이라는 소박한 쿨다운만으로도 대부분이 최종 사용자에게 도달하는 것을 막을 수 있었을 것이다.
이 개념은 도구에 따라 다른 이름으로 불린다(
cooldown
,
minimumReleaseAge
,
stabilityDays
,
exclude-newer
). 또한 구현도 제각각이다. 롤링 기간을 쓰는지 절대 타임스탬프를 쓰는지, 전이 의존성까지 포함하는지 직접 의존성만 다루는지, 보안 업데이트는 예외로 둘지 등에서 차이가 난다. 하지만 지난 1년간의 도입 속도는 놀라울 정도로 빠르다.
JavaScript 생태계는 누구보다도 빠르게 움직였다. pnpm은 2025년 9월의 10.16 버전에서
minimumReleaseAge
를 출시했고, 직접 및 전이 의존성을 모두 대상으로 하면서, 충분히 신뢰해 건너뛸 패키지를 위한
minimumReleaseAgeExclude
목록도 제공한다. Yarn은 같은 달 4.10.0 버전에서
npmMinimalAgeGate
를 출시했다(단위도 분이며,
npmPreapprovedPackages
로 예외를 둔다). 이어서 Bun은 2025년 10월 1.3 버전에서
bunfig.toml
을 통해
minimumReleaseAge
를 추가했다. npm은 더 오래 걸렸지만 2026년 2월 11.10.0 버전에서
min-release-age
를 출시했다. Deno에는
deno update
와
deno outdated
에 대한
--minimum-dependency-age
가 있다. 6개월 동안 5개의 패키지 매니저라니, 경쟁하는 도구들 사이에서 이렇게 조율된 기능 도입의 전례는 떠오르지 않는다.
uv는 초기부터 절대 타임스탬프용
--exclude-newer
를 제공했고, 2025년 12월 0.9.17 버전에서 상대 기간 지원(예:
1 week
,
30 days
)을 추가했다. 또한
exclude-newer-package
로 패키지별 오버라이드도 지원한다. pip는 2026년 1월 26.0 버전에서 plaintext --uploaded-prior-to 를 출시했지만, 절대 타임스탬프만 받으며 상대 기간 지원을 추가하자는 open issue가 있다.
Bundler와 RubyGems에는 네이티브 쿨다운 지원이 없지만, 커뮤니티가 운영하는 gem 서버인 gem.coop이 쿨다운 베타를 출시해 별도의 엔드포인트에서 제공되는 새로 게시된 gem에 48시간 지연을 강제한다. 클라이언트가 아니라 인덱스 수준에서 쿨다운을 거는 접근은 흥미롭다. gem.coop 엔드포인트를 바라보는 Bundler 사용자는 도구나 워크플로를 전혀 바꾸지 않고도 쿨다운을 얻기 때문이다.
Cargo는 진행 중인 RFC가 있고, 쿨다운을 위한 레지스트리 측 인프라는 Cargo 1.94에서 안정화됨 (2026년 3월 5일 릴리스) 상태다. 이들의 접근은 예외 목록 문제를 아예 피해 간다. 쿨다운에서 패키지를 예외 처리하는 대신,
cargo update foo --precise 1.5.10
로 새 버전에 대해 명시적으로 옵트인하고, 그 선택을 lockfile에 기록한다. 나중에 정리해야 할 exclude 목록이 없다. 그 사이에 개념 증명으로 개발자 머신에서 설정 가능한 쿨다운 창을 강제하는 서드파티 래퍼인 cargo-cooldown도 있다. Go에는
go get
과
go mod tidy
에 대한 open proposal이 있고, Composer에는 두개의 open 이슈가 있다. NuGet에도 open issue가 있지만, Dependabot을 사용하는 .NET 프로젝트는 Dependabot이 2025년 7월에 NuGet 지원을 확장하면서 업데이트 봇 측에서 이미 쿨다운을 적용받는다.
Renovate는 수년 전부터
minimumReleaseAge
(원래 이름은
stabilityDays
)를 지원해 왔고, 설정된 시간이 지날 때까지 업데이트 브랜치에 “pending” 상태 체크를 추가했다. Mend Renovate 42는 한 걸음 더 나아가, “best practices” 설정에서 npm 패키지에 대해 3일 최소 릴리스 경과 시간을 기본값으로 만들었다.
security:minimumReleaseAgeNpm
프리셋을 통해 사용자에게 쿨다운이 옵트인이 아니라 옵트아웃이 되게 한 것이다. Dependabot는 2025년 7월에
dependabot.yml
의
cooldown
블록으로 쿨다운을 출시했다. 여기에는
default-days
와 semver 수준별 오버라이드(
semver-major-days
,
semver-minor-days
,
semver-patch-days
)가 있으며, 보안 업데이트는 쿨다운을 우회한다. Snyk는 자동 업그레이드 PR에 대해 내장된, 설정 불가능한 21일 쿨다운으로 가장 공격적인 입장을 취한다. npm-check-updates는
--cooldown
파라미터를 추가했으며
7d
또는
12h
같은 기간 접미사를 받는다.
zizmor는 1.15.0 버전에서
dependabot-cooldown
감사 규칙을 추가해, 쿨다운 설정이 없거나 쿨다운 기간이 불충분한(기본 임계값: 7일) Dependabot 설정을 표시하며 자동 수정도 지원한다. StepSecurity는 설정 가능한 쿨다운 기간 내에 릴리스된 npm 패키지를 도입하는 PR을 실패 처리하는 GitHub PR 체크를 제공한다. OpenRewrite는 Dependabot 설정 파일에 쿨다운 섹션을 자동으로 추가하는
AddDependabotCooldown
레시피를 제공한다. GitHub Actions에 한정하면, pinact는
--min-age
플래그를 추가했고, prek(pre-commit의 Rust 재구현)은
--cooldown-days
를 추가했다.
Go, Bundler, Composer, pip의 경우 쿨다운 지원이 여전히 논의 중이거나 부분적으로만 들어온 상태라, 지연을 강제하려면 Dependabot이나 Renovate에 의존하게 된다. 이는 자동 업데이트는 커버하지만, 누군가 로컬에서
bundle update
나
go get
을 실행해 레지스트리에 올라온 지 10분 된 버전을 끌어오는 것을 막지는 못한다. Maven, Gradle, Swift Package Manager, Dart의 pub, Elixir의 Hex에 대해서는 쿨다운 논의를 전혀 찾지 못했다. 관련 정보를 알고 있다면 알려 달라. 이 글을 업데이트하겠다.
또한 이 기능은 지원하는 도구들 사이에서도 최소 10가지 서로 다른 설정 이름으로 불린다(
cooldown
,
minimumReleaseAge
,
min-release-age
,
npmMinimalAgeGate
,
exclude-newer
,
stabilityDays
,
uploaded-prior-to
,
min-age
,
cooldown-days
,
minimum-dependency-age
). 그래서 이에 대해 글을 쓰는 일은, 다국어(polyglot) 프로젝트 전반에서 이를 설정하는 일만큼이나 어렵다.
npm, PyPI, RubyGems에서는
npm publish
나
gem push
를 실행하면 몇 초 만에 전 세계에서 설치 가능한 패키지가 된다. 그리고 그 창에서 Dependabot이나 Renovate가 우연히 실행되기라도 하면, 인간이 한 번도 보지 못한 채 악성 코드가 프로젝트에 들어간다. William이 살펴본 모든 공급망 공격은 이 속성을 악용한다. 즉, 게시(publishing)와 배포(distribution)가 동일한 행위이고, 손상된 메인테이너 계정과 수천 개의 다운스트림 프로젝트 사이에 아무것도 없다는 점이다.
시스템 패키지 매니저는 두 가지를 분리하기 때문에 다르게 동작한다. 누군가 업스트림 라이브러리의 새 버전을 올려도, 배포판 메인테이너가 변경을 검토하고 패키지 정의를 업데이트한 다음 빌드 파이프라인을 통과시키기 전까지는
apt install
이나
brew install
에 나타나지 않는다. Fedora 패키지는 리뷰와 koji 빌드를 거치고, Homebrew는 CI를 통과한 풀 리퀘스트가 필요하며 메인테이너가 이를 머지한다. 업스트림 tarball이 손상되었더라도, 누군가의 머신에 도달하기 전에 그 과정을 통과해야 한다. 또한 리뷰를 수행하는 사람들은 패치가 난독화된 postinstall 스크립트를 추가해 원격 페이로드를 curl로 받아오는 경우를 대개 알아차린다.
Debian은 더 나아간다. 메인테이너 계정이 손상되더라도 업로드는 먼저 unstable로 들어가고, 그 뒤 긴급도와 패키지 테스트 가용성에 따라 2~10일 후 testing으로 자동 이동(automigrate)된다. stable은 별도의 릴리스 절차를 통해서만 업데이트를 받는다. 이는 여러 단계의 사람 검토가 포함된 사실상의 내장 쿨다운이다.
언어 패키지 매니저 측의 쿨다운은, 원래 그런 검토 창이 없던 생태계에 그와 비슷한 창을 사후적으로 덧대려는 시도다. 자동화 도구가 lockfile로 끌어오기 전에 보안 연구자가 악성 배포를 표시할 며칠을 벌어준다. 반대로 Homebrew나 apt에 동일한 기능을 추가하자고 하는 것은, 이미 사람 게이트키퍼가 있는 과정에서 보안 패치를 지연시키는 일이 되며, 절약되는 것보다 비용이 더 든다.
pip의
--uploaded-prior-to
와 npm의 오래된
--before
플래그는 모두 절대 타임스탬프를 받는다. 그리고 pip에 상대 기간 지원을 추가하자는 논의를 보면, 이 두 모드가 구현 표면은 공유하지만 서로 다른 목표를 위해 봉사한다는 점이 드러난다. 절대 타임스탬프는 의존성 해석을 특정 시점에 고정하므로, 6개월 뒤에 같은 설치를 실행해도 같은 결과가 나오게 한다. 이는 재현성 기능이다. 반면
7 days
같은 상대 기간은 당신과 함께 앞으로 이동하는 슬라이딩 윈도우를 만들어, 빌드를 언제 실행하든 최근에 게시된 패키지를 항상 제외한다. 이는 보안 기능이다. uv의
--exclude-newer
는 두 형태를 모두 받으며, npm은 절대 날짜용
--before
와 상대 기간용
min-release-age
를 모두 갖고 있다. pnpm, Yarn, Bun, Deno는 상대 기간만 받는다.
pip 스레드에서는 기간 문자열을 파싱하는, 놀라울 정도로 성가신 문제도 다룬다. ISO 8601 기간(
P7D
)은 모호하지 않지만 아무도 입력하고 싶어 하지 않고, 사람이 읽기 쉬운 문자열인
7 days
는 친절하지만 pip 메인테이너가 직접 작성하고 유지보수하고 싶지 않은 파서가 필요하다. 또한 월/년 같은 길이가 가변인 달력 단위는 구체적인 일수로 변환하려면 현재가 몇 월인지 알아야 한다. uv는 ISO 8601과 친절한 문자열을 모두 선택하되 월과 년은 아예 제외했다. pip 메인테이너들은 윤년 계산까지 끌어들이지 않으면서 거의 모든 실제 사용 사례를 커버할 수 있는 “일(day) 수의 맨숫자”만 받는 쪽으로 기울고 있다.
심지어 “7일 전”이 무엇을 의미하는지도, CI 서버는 UTC에 있고 개발자 노트북은 미국 태평양 시간에 있으며 레지스트리 타임스탬프는 PyPI 서버가 설정된 임의의 타임존을 사용하는 상황에서는 복잡해진다. 타임존 드리프트 몇 시간 차이로, 6일 22시간 전에 게시된 패키지가 쿨다운 체크를 통과하느냐가 결정될 수 있다.