Deno 2.6's minimum dependency age flag ignores year and month durations
Deno 2.6의 최소 의존성 사용 기간 플래그가 연·월 단위를 무시합니다
Deno의 npm 의존성 최소 게시 기간 기능은 일·주·분 단위에서는 동작하지만, 월·년이 포함된 ISO-8601 기간을 지정하면 필터를 조용히 무시하고 최신 버전을 설치합니다. 또한 lockfile에 이미 고정된 버전에는 적용되지 않으며, deno audit은 TLS 프록시 환경에서 별도의 인증서 오류를 일으킬 수 있습니다.
- 주제
AI 요약
Deno 2.6은 npm 공급망 공격을 줄이기 위해, 일정 기간 이상 게시된 의존성만 설치하도록 제한하는 기능을 추가했습니다. 악성 패키지 버전은 배포 후 수 시간이나 수일 안에 신고·삭제되는 경우가 많으므로, 패키지가 일정한 대기 기간을 통과한 뒤에만 설치하면 이 유형의 공격에 노출될 가능성을 낮출 수 있다는 구상입니다. 이 기능은 `deno install`과 `deno add`에서 사용할 수 있으며, 작성자는 해당 기능을 포함한 현재 안정 버전 Deno 2.9.6에서 실제 npm 패키지를 대상으로 동작을 확인했습니다.
■ 30일 제한은 대체로 정상적으로 동작합니다
관련 CLI 옵션은 `--min-dep-age`이며, 분 단위 숫자, ISO-8601 duration, RFC3339 절대 시각을 받을 수 있다고 표시됩니다. 예를 들어 `120`은 2시간, `P2D`는 2일, `2025-09-16`은 기준 날짜, `2025-09-16T12:00:00+00:00`은 기준 시각을 의미하며 `0`은 제한을 끕니다. 작성자는 실제 JavaScript 프로젝트에서 자주 사용하는 패키지 20개를 새 프로젝트에 한 번에 추가하면서, 제한이 없는 경우와 `--min-dep-age=P30D`를 지정한 경우를 비교했습니다. 두 실행 모두 lockfile이 없는 동일한 상태에서 진행했고, 바꾼 것은 해당 플래그뿐입니다.
20개 중 12개는 30일 정책을 적용했을 때 더 오래된 버전으로 내려갔습니다. `axios`는 1.20.0에서 1.19.0으로, `eslint`는 10.10.0에서 10.8.1로, `jest`는 30.5.1에서 30.4.2로, `next`는 16.3.5에서 16.3.1로 바뀌었습니다. `react`는 19.3.0에서 19.2.8로, `rollup`은 4.63.3에서 4.62.4로, `uuid`는 14.0.2에서 14.0.1로, `vite`는 8.3.0에서 8.2.1로 내려갔습니다. `vitest`는 5.0.1에서 4.1.10으로, `vue`는 3.5.42에서 3.5.41로, `webpack`은 5.111.0에서 5.109.2로, `zod`는 4.6.5에서 4.4.3으로 변경됐습니다. 나머지 `chalk`, `commander`, `dayjs`, `esbuild`, `express`, `lodash`, `prettier`, `typescript`는 최신 릴리스 자체가 이미 30일보다 오래되어 버전이 달라지지 않았습니다.
특히 `vitest`에서는 정책의 영향이 크게 나타났습니다. 테스트 시점의 최신 버전 5.0.1은 게시된 지 1.8일밖에 되지 않았고, 30일 조건을 통과하는 최신 버전은 4.1.10이었습니다. 따라서 새 프로젝트에서 `deno add npm:vitest --min-dep-age=P30D`를 실행하면 오류나 경고 없이 이전 메이저 버전이 선택됩니다. 최소 게시 기간 정책이 활성화된 사실을 모르면 최신 메이저 버전이 저장소에 없는 것으로 오해할 수 있습니다.
성능 비용은 거의 측정되지 않았습니다. 캐시가 비어 있는 새 Docker 컨테이너에서 20개 패키지를 제한 없이 설치하는 데 10.2초, 30일 필터를 적용하는 데 10.6초가 걸렸습니다. 작성자는 필터링이 npm이 이미 반환하는 메타데이터를 비교하는 과정이므로 비용이 거의 없다고 설명합니다.
■ 월·년을 포함한 ISO-8601 duration은 제한을 무시합니다
문제는 ISO-8601 duration의 모든 형식이 지원되는 것처럼 CLI 설명이 보이지만 실제로는 일·주·분 단위만 제대로 처리한다는 점입니다. CLI 도움말에는 `P2D`처럼 일 단위 예시만 나오지만, 옵션 설명에는 월과 년 단위가 지원되지 않는다는 안내가 없습니다.
작성자가 `zod`를 대상으로 확인한 결과, `--min-dep-age=P7D`에서는 게시 후 7.3일이 지난 `zod` 4.6.1이 선택됐고, `--min-dep-age=P30D`에서는 게시 후 135.9일이 지난 4.4.3이 선택됐습니다. 그러나 `--min-dep-age=P1M`을 지정하자 게시 후 3.2일밖에 지나지 않은 실제 최신 버전 4.6.5가 설치됐습니다. `P2Y`와 `P100Y`를 사용했을 때도 결과는 동일하게 최신 버전 4.6.5였습니다.
일·주 단위인 `P30D`와 `P4W`는 모두 설치 버전을 올바르게 제한했고, 일반적인 분 단위 값과 절대 날짜도 동작했습니다. 예를 들어 `--min-dep-age=2024-01-01`은 훨씬 오래된 `zod` 3.22.4를 선택했습니다. 반면 duration에 월이나 년 구성요소가 포함되면 전체 제한이 사라졌습니다. `P0Y1M0D`, `P1Y0M0D` 같은 값뿐 아니라 `P1DT0H`처럼 시간 구성요소를 포함한 값도 같은 방식으로 실패했습니다. 모든 경우 종료 코드는 0이었고 stderr에도 아무 내용이 출력되지 않았습니다. `--log-level=debug`에서도 후보 버전이 거부됐다는 기록은 보이지 않고, resolver가 해당 버전을 선택한 내용만 표시됐습니다.
따라서 조직 정책을 “한 릴리스 주기보다 새로운 의존성은 허용하지 않음”처럼 월 단위로 표현해 `deno.json`에 `"minimumDependencyAge": "P6M"`으로 저장하면, 아무런 보호도 적용되지 않습니다. 정책이 활성화되지 않았다는 사실을 알기 위해서는 설치된 버전을 npm registry와 직접 비교해야 합니다. 현재 기능을 사용하려면 원문은 정책을 일 또는 주 단위로 표현하고, `deno add` 이후 `deno.json`과 `deno.lock`의 변경 내용을 확인하라고 안내합니다.
■ CLI 플래그와 deno.json 설정 키의 차이
CLI 옵션 이름은 `--min-dep-age`이지만 `deno.json`에서 사용하는 키는 이를 그대로 변환한 `minDepAge`가 아닙니다. 설정 파일에서는 이름을 모두 풀어 쓴 `minimumDependencyAge`를 사용해야 합니다.
```json { "minimumDependencyAge": "P30D" } ```
작성자는 처음에 플래그 이름에 맞춰 `minDepAge`를 설정 키로 사용했지만, 이 값은 경고 없이 무시됐습니다. Deno 공식 블로그의 기능 소개에는 이미 `minimumDependencyAge`라는 정확한 키가 사용되고 있어 문서 자체는 올바르지만, CLI 플래그 이름에서 설정 키를 추론하면 제한 없는 설치가 발생할 수 있습니다.
■ lockfile에 이미 선택된 버전에는 적용되지 않습니다
최소 의존성 기간 검사는 패키지 버전을 새로 선택하는 resolver 단계에서만 실행됩니다. 작성자는 제한 없이 `npm:[email protected]`를 lockfile에 기록한 뒤 `node_modules`를 삭제하고, 같은 프로젝트에서 `deno install --min-dep-age=P30D`를 실행했습니다. Deno는 게시된 지 3.2일밖에 되지 않은 `zod` 4.6.5를 그대로 다시 설치했으며, 30일 조건 위반에 대한 오류나 경고를 출력하지 않았습니다.
이는 재현 가능한 빌드를 위해 설치 단계에서 lockfile의 버전을 바꾸지 않는 설계로 볼 수 있지만, CI의 install 단계에만 `--min-dep-age`를 추가하는 것으로는 기존 lockfile을 보호할 수 없습니다. 누군가 제한 없이 버전을 선택해 커밋한 뒤라면, 이후의 `deno install`은 해당 버전을 그대로 설치합니다. 보호 기능은 패키지 버전을 선택하는 시점에 존재하며, 이미 선택된 버전을 설치하는 시점에는 다시 검증되지 않습니다.
■ approve-scripts와 deno audit 확인 결과
Deno 2.6의 또 다른 변경 사항은 `deno install --allow-scripts`를 대신해 npm postinstall hook을 더 세밀하게 허용하는 `deno approve-scripts`를 도입한 것입니다. `nodeModulesDir`를 `auto`로 설정한 프로젝트에서 postinstall script가 있는 실제 패키지 `simple-git-hooks`를 설치하자, 스크립트는 기본적으로 실행되지 않았고 다음과 같은 경고가 표시됐습니다. 패키지의 build script가 무시됐으며 실행하려면 `deno approve-scripts`를 사용하라는 내용입니다.
이후 `deno approve-scripts npm:simple-git-hooks`를 비대화식으로 실행하자 패키지가 승인됐고, `[email protected]`의 `postinstall` script가 실행됐습니다. 승인 정보는 `deno.json`에 `"allowScripts": ["npm:simple-git-hooks"]`로 기록됐습니다. 이 동작은 문서와 일치했으며, 기본 차단·명시적 승인·읽을 수 있는 영구 설정이라는 흐름이 확인됐습니다.
반면 `deno audit`은 TLS inspection proxy 환경에서 별도의 네트워크 오류를 보였습니다. 알려진 취약 버전인 `[email protected]`를 설치한 뒤 실행하자 npm advisory database에 요청하는 과정에서 `invalid peer certificate: UnknownIssuer` 오류가 발생했습니다. 처음에는 프록시 환경 자체의 문제일 수 있다고 판단했지만, 동일한 컨테이너와 인증서 설정에서 동일한 URL에 대해 Deno의 `fetch()`를 실행하자 HTTP 200과 advisory JSON을 정상적으로 받았습니다. 같은 실행에서 일반 npm installer도 `registry.npmjs.org`와 통신했습니다.
따라서 작성자는 `deno audit`이 CLI의 다른 네트워크 경로와 별도의 HTTP client를 사용하며, 동일한 인증서 신뢰 설정을 가져오지 못하는 것으로 보인다고 설명합니다. 특히 기업이나 CI 환경에서 TLS를 종료하는 프록시를 사용하는 경우, 다른 Deno 네트워크 요청은 성공해도 `deno audit`만 실패할 수 있으므로 CI gate에 넣기 전에 별도로 확인해야 합니다.
■ 재현 방법과 적용 시 주의점
재현에는 Docker와 `registry.npmjs.org`에 대한 외부 연결이 필요합니다. 새 디렉터리에서 `deno.json`을 만든 뒤 `denoland/deno:latest` 컨테이너로 `deno add npm:zod`를 실행하고, 이어서 `--min-dep-age=P7D`, `--min-dep-age=P1M`을 각각 적용해 결과 버전을 비교하면 됩니다. P7D 실행은 기본 실행보다 오래된 버전을 선택해야 하고, P1M 실행은 기본 실행과 똑같이 최신 버전을 선택해야 합니다. TLS inspection proxy 뒤에 있다면 `--network host`와 `DENO_CERT` 환경 변수, CA bundle 마운트가 추가로 필요합니다.
현재 `--min-dep-age` 또는 `minimumDependencyAge`를 도입할 때는 월·년 대신 일·주 단위를 사용하고, 의존성을 추가하거나 업데이트한 뒤 실제 `deno.json`과 `deno.lock` diff를 확인해야 합니다. 이미 커밋된 lockfile을 CI 설치 단계에서 이 옵션만으로 보호할 수 없다는 점도 함께 고려해야 합니다. 해당 기능은 Deno에서 아직 Unstable로 표시되어 있으므로, 팀 정책에 포함하기 전에 기간 형식, lockfile 동작, 프록시 환경의 `deno audit` 결과를 각각 검증해야 합니다.
원문: dev.to / 번역·요약: Trawling