주요 내용
- 전환적 종속성이란, 자신이 의존하는 패키지가 필요로 하기 때문에 함께 설치하게 되는 패키지를 말합니다.
- 하나의 npm 패키지가 수백 개의 전이적 종속성을 끌어들일 수 있으며, 각 종속성은 사용자의 권한으로 코드를 실행할 수 있습니다.
- 다른 유지보수 담당자들은 소스 트리의 깊은 곳에 범위를 설정해 두기 때문에, 수정 사항과 공격은 그들만의 일정에 따라 발생합니다.
- 팀이 검토한 적이 없는 패키지까지 보호하려면 레지스트리 수준에서 보호 기능이 작동해야 합니다.
몇 달 이상 진행해 온 프로젝트라면 무엇이든 이 방법을 시도해 보세요. 열기 package.json 그리고 종속성을 집계합니다. 그런 다음 다음 명령을 실행합니다. npm ls --all 그리고 얼마나 많은 내용이 지나가는지 지켜보세요.
다음 목록에서 package.json 보통 수십 개 정도입니다. npm이 중복 항목을 제거하더라도 트리 구조는 훨씬 더 길어집니다. 이 트리 구조에서 사용자가 직접 나열하지 않은 모든 항목은 전이적 의존성이며, 이는 스택 내에서 가장 적은 관심을 받으면서도 막대한 양의 작업을 수행하는 부분입니다.
타동사 의존 관계가 실제로 무엇인지
직접 의존성이란 사용자가 직접 요청한 의존성을 말합니다. 사용자가 다음 명령을 실행했을 때 npm install express, 그곳에 떨어졌다 package.json, 그리고 팀원 중 누군가가 이에 대해 결정을 내렸습니다.
타동적 의존성이란 사용자가 직접 요청하지 않은 의존성을 말합니다. Express에는 자체 패키지가 필요하고, 그 패키지들에는 또 다른 패키지가 필요하며, npm이 이 전체 체인을 대신 해결해 줍니다. 이 중 어느 것도 프로젝트의 package.json, 하지만 이 모든 내용은 락파일에 기록되며, 실제로 설치되는 항목은 node_modules. 설치 중이든 앱이 이를 불러올 때든, 실행되는 모든 코드는 이를 실행하는 주체와 동일한 접근 권한을 갖게 됩니다. 노트북에서는 사용자 계정을 의미하며, CI 환경에서는 러너와 러너가 볼 수 있는 모든 시크릿을 의미합니다.
이 개념에 이름을 붙이는 것이 도움이 되므로, 이를 ‘상속된 위험’이라고 부르기로 합시다. 상속된 위험이란, 여러분이 선택한 패키지와 함께 제공된 다른 패키지들로부터 프로젝트가 감수하게 되는 위험을 말합니다. 여러분은 이 패키지들을 평가하지도 않았고, 버전을 선택하지도 않았으며, 대부분의 경우 그 이름을 알 수도 없었습니다. 그럼에도 불구하고 이 패키지들은 프로젝트에 포함되어 배포됩니다.
나무가 왜 그렇게 커지는 걸까
이는 부분적으로 자바스크립트 생태계가 성장해 온 방식과도 관련이 있습니다. npm 덕분에 단일 용도의 작은 패키지를 쉽게 공개할 수 있게 되었고, 이러한 문화가 자연스럽게 확산되었습니다. 한 가지 기능을 잘 수행하는 유틸리티가 수백 개의 다른 패키지에 포함되게 되고, 각 패키지는 또 저마다의 작은 보조 기능을 함께 가져오게 됩니다. TuxCare의 ‘SecureChain for npm’ 페이지에서 설명하듯이, 하나의 최상위 npm 패키지가 수백 개의 전이적 의존성을 불러올 수 있습니다.
또 다른 부분은 npm이 어떻게 구성되어 있는지입니다 node_modules. 가능한 곳이라면 어디에서든, npm 호이스트가 소나무 위로 소포들을 들어 올린다 그래서 대부분은 상단 부근에 나란히 배치되어 있습니다. 이는 디스크 공간 절약에는 좋지만, 직관성 측면에서는 최악입니다. 스크롤하여 node_modules 그리고 그 패키지가 4단계나 깊게 중첩되어 있으며, 당신이 들어본 적도 없는 무언가가 요청했기 때문에 거기에 존재한다는 사실을 알려주는 것은 아무것도 없습니다.
레이어를 보려면, npm explain <package> 주어진 패키지로 이어지는 모든 경로를 보여줍니다. lockfile에 있는 임의의 항목을 대상으로 이 명령을 실행해 보세요. 해당 패키지로 이어지는 경로가 하나 이상 나와도 놀라지 마세요.
해당 버전 범위는 귀하가 소유하고 있지 않습니다.
이 부분이 많은 사람들을 혼란스럽게 합니다. 의존성을 추가할 때 해당 의존성의 버전 범위를 선택하게 됩니다. 그 의존성이 또 다른 의존성을 추가하면, 해당 의존성의 유지보수자가 버전 범위를 선택하고, 여러분은 이를 그대로 적용받게 됩니다.
이러한 범위 중 대부분은 캐럿 기호를 사용하는데, 예를 들어 ^4.17.0, 이는 semver는 이를 다음과 같이 취급합니다. 4.17.0 이상의 4.x 버전 중 아무 것이나. 귀하의 잠금 파일 지난번에 문제가 해결되었던 정확한 버전을 고정해 두기 때문에, 이를 커밋하고 npm ci CI에서는 이 점이 매우 중요합니다. 하지만 누군가가 잠금 파일을 삭제하고 다시 생성하거나, 업데이트를 실행하거나, 잠금 파일이 없는 프로젝트에 설치하는 순간, 트리 전반에 걸친 범위 지정(ranges)은 허용되는 최신 버전(1시간 전에 공개된 버전 포함)으로 해결될 수 있습니다.
유지보수 담당자가 수정 사항을 배포할 때는 편리합니다. 하지만 다른 것을 배포할 때는 문제가 됩니다.
공격자들은 이 사실을 이미 오래전에 알아차렸습니다. 2018년 당시, 인기 있는 event-stream 패키지 flatmap-stream이라는 종속성을 추가했습니다., 이 앱에는 특정 비트코인 지갑 앱에서 자금을 탈취하기 위한 코드가 포함되어 있는 것으로 밝혀졌습니다. 이 앱을 사용하는 사람은 아무도 없었지만 event-stream 선택했었다 flatmap-stream. 방금 도착했어요.
2026년 6월에도 동일한 수법이 다시 나타났습니다. 공격자들이 Mastra npm 스코프에 대한 게시 권한을 확보하자, 그들은 다음을 푸시했습니다. 140개 이상의 보안 취약점이 발견된 패키지 버전 The Hacker News의 보도에 따르면 2시간도 채 되지 않아, 그리고 소켓. Mastra 코드 자체는 변경되지 않았습니다. 변경된 부분은 의존성 목록이었는데, 이제 여기에는 다음이 포함되었습니다. easy-day-js, ~와 닮은 dayjs date 라이브러리. 누가 통제했든 간에 easy-day-js 전날 오류 없이 정상 작동하는 1.11.21 버전을 공개한 뒤, 6월 17일 이른 아침, Mastra 버전이 출시되기 직전에 악성 포스트인스톨 후크가 포함된 1.11.22 버전을 배포했습니다. 새로운 Mastra 버전은 ^1.11.21, 그래서 새로 설치할 때마다 바로 문제가 있는 릴리스로 연결되었습니다. 만약 그 릴리스 중 하나를 검토했다면, 실제로 발견할 수 있었던 유일한 변경 사항은 package.json 단 하나의 새로운 종속성뿐이었습니다. 위험한 요소들은 모두 한 단계 아래에 위치해 있었습니다.
심각한 취약점을 해결하는 데 왜 그렇게 오랜 시간이 걸리는가
알려진 취약점들은 의존성 트리의 깊은 곳에도 쌓여 있습니다. 엔도르 랩스(Endor Labs)가 2022년 ‘의존성 관리 현황(State of Dependency Management)’ 보고서를 위해 널리 사용되는 자바 패키지를 분석한 결과, 취약점이 있는 의존성 중 약 95%가 전이적(transitive)인 것으로 나타났습니다. 이는 메이븐(Maven) 패키지였으므로, 이 수치를 npm의 실제 수치로 받아들이기보다는 npm의 규모를 가늠하는 참고 자료로 삼으시기 바랍니다.
가령, 여러분이 선택한 패키지보다 세 단계 아래에 있는 패키지에 CVE가 발생했다고 가정해 봅시다. 수정 사항이 업스트림에서 공개되면, 이상적인 상황에서는 여러분의 트리에서 이를 자동으로 반영하게 됩니다. 하지만 실제로는, 여러분과 수정 사항 사이에 있는 모든 패키지가 새 버전을 허용해야만 이 과정이 원활하게 진행됩니다. 체인 내의 상위 패키지 중 어느 하나라도 정확한 버전을 고정해 두었거나, 수정된 릴리스보다 낮은 버전 범위로 제한해 둔 경우, 일반적인 해결 과정은 바로 그 지점에서 중단됩니다. 이제 해당 패키지 관리자가 릴리스를 출시하기를 기다려야 하며, 경우에 따라 그 상위 관리자의 릴리스도 기다려야 할 수 있습니다.
바로 이것이 그 이유입니다 npm audit 정말 답답할 수 있습니다. 이 도구는 트리 구조의 깊은 곳에서 무언가 문제가 있다는 사실을 알려주는 데는 유용합니다. 하지만 문제를 해결하는 데는 훨씬 더 취약한데, 해결책은 대개 여러분이 직접 마련할 수 있는 것이 아니기 때문입니다. npm audit fix 다음이 없으면 semver-major 변경을 적용하지 않습니다. --force, 그리고 --force 앱의 어느 부분이 오류가 발생하는지 파악하는 데 꽤 직설적인 방법입니다.
그리고 또 재정의, 이는 npm은 버전 8.3에서 추가되었습니다.. 오버라이드를 사용하면 상위 패키지가 요청한 내용과 관계없이, 전이적 패키지의 특정 버전을 강제로 적용하거나 포크된 버전을 대신 사용할 수 있습니다. 이를 통해 취약점을 해결할 수 있습니다. 다만, 어떤 조치를 취했는지 명확히 밝혀두어야 합니다. 이제 상위 패키지 관리자가 아마도 테스트해 본 적 없는 패키지 조합을 실행하게 되며, 오버라이드 설정은 대개 귀하의 package.json 그것이 왜 거기에 있는지 아무도 기억하지 못하게 된 지 한참 뒤에도.
다시 돌아오지 않을 유지보수 담당자
이 모든 것은 상류에서 누군가가 여전히 릴리스를 배포하고 있다는 전제 하에 성립합니다. 대규모 소스 트리 깊숙한 곳에서는 항상 그런 것은 아닙니다.
트리 하단에 위치한 패키지 중 상당수는 규모가 작고 사실상 개발이 완료된 상태이며, 일부 유지보수자는 이미 수년 전에 자리를 떠났습니다. 이러한 패키지 중 하나에 CVE가 발생하면 수정 코드를 작성해 줄 사람이 없어, 상위 단계로 전파할 수 있는 내용이 없습니다. 스캐너는 계속해서 해당 문제를 표시하지만, 오버라이드에서 참조할 수 있는 업스트림 수정 사항은 존재하지 않습니다. 남은 방법은 포크하여 직접 패치를 적용하거나, 수정 사항을 백포트해 줄 사람을 찾거나, 해당 패키지에 의존하는 최상위 레벨 패키지를 대체하는 것뿐입니다. 이 중 어느 방법도 신속하게 해결될 수 없으며, 마지막 방법은 아무도 시간을 낼 수 없는 재작성으로 이어질 수 있습니다.
백포트(backport) 방식은 바로 TuxCare의 ‘무한 수명 주기 지원(Endless Lifecycle Support, ELS) ’이 존재하는 이유입니다. ELS는 지원 대상 패키지에 대해, 업스트림 유지보수가 종료된 후에도 사용자가 현재 실행 중인 정확한 버전에 보안 수정 사항을 백포트해 주므로, 사용자는 강제로 업그레이드할 필요 없이 기존 환경을 그대로 유지할 수 있습니다.
나무 전체를 보호하기
위의 수정 사항 대부분은 프로젝트 내에서, 한 패키지씩, 대개 문제가 이미 프로젝트에 반영된 후에 이루어집니다. 오버라이드, 감사 수정, 수동 고정 등은 모두 업스트림에서 유입된 문제를 정리하는 방법입니다.
대안은 전체 패키지 트리의 출처를 변경하는 것입니다. 이것이 바로 SecureChain의 핵심 개념입니다. SecureChain은 공개 npm 레지스트리를, 설치 시 트랜지티브 패키지를 포함하여 해결되는 엄선된 저장소로 대체합니다. 패키지는 보안이 강화되고 격리된 환경에서 검증된 소스 코드를 기반으로 재빌드되며, 알려진 악성코드 및 의심스러운 코드 패턴에 대한 검사를 거칩니다. 검사에서 불합격된 항목은 사용자의 환경에 도달하기 전에 차단됩니다. 패키지 트리에 필요한 항목이 아직 카탈로그에 없는 경우, TuxCare가 이를 추가합니다. 사용 중인 패키지는 새로운 CVE가 발표될 때마다 SLA에 따라 패치되며, SecureChain이 관리하는 패키지의 지원 기간이 종료되더라도 ELS가 자동으로 지원을 이어가므로 사용자의 별도 조치가 필요하지 않습니다.
npm이나 리포지토리 관리 도구를 SecureChain에 연결하기만 하면, 일상적인 업무에는 아무런 변화가 없습니다. npm, Yarn, pnpm은 기존과 동일한 명령어를 유지하며, 팀에서 이미 Artifactory, Nexus 또는 GitHub Packages를 통해 패키지를 중계하고 있다면 SecureChain도 이와 연동되어 작동합니다. 달라지는 점은, 아무도 들어본 적 없는 4단계 아래에 있는 패키지조차도 여러분이 의도적으로 선택한 프레임워크와 마찬가지로 동일한 재빌드 및 심사 과정을 거친다는 것입니다.
어디서부터 시작해야 할까
무언가를 변경하기 전에, 현재 어떤 환경이 실행 중인지 명확히 파악하십시오. 실행하십시오. npm ls --all 그리고 나중에 비교할 수 있도록 결과를 저장해 두세요. 잘 모르는 패키지 몇 개를 골라 다음 명령을 실행하세요. npm explain 그것들에 대해. lockfile이 커밋되었는지, 그리고 CI가 npm ci, 따라서 빌드 과정에서는 범위(range)를 다시 해결하는 대신 락파일(lockfile)을 기반으로 설치가 이루어지며, 누군가가 업데이트된 락파일을 커밋해야만 버전이 변경됩니다.
그런 다음 최근 몇 건의 감사 보고서를 살펴보고, 감사 결과를 ‘직접 선정한 항목’과 ‘기존에 물려받은 항목’으로 분류해 보세요. 두 번째 분류는 기존에 물려받은 위험이 실제로 어디에 위치해 있는지를 잘 보여주는 지표이며, 가장 면밀히 살펴볼 가치가 있는 부분입니다.
자주 묻는 질문
타동사 의존 관계란 무엇인가요?
전이적 의존성은 사용자가 직접 추가한 것이 아니라, 프로젝트의 의존성 중 하나가 이를 필요로 하기 때문에 프로젝트가 설치하는 패키지를 말합니다. npm에서는 전이적 의존성이 lockfile에 기록되며, 설치가 완료되면 node_modules, 하지만 프로젝트의 package.json.
직접 의존성과 전이적 의존성의 차이점은 무엇인가요?
직접 종속성은 프로젝트가 자체적으로 선언한 패키지들을 말합니다. package.json. 전이적 의존성은 트리 구조의 모든 수준에서 해당 패키지가 불러오는 모든 것을 말합니다. 직접 의존성의 버전 범위는 사용자가 직접 제어할 수 있는 반면, 전이적 의존성의 버전 범위는 상위 패키지에 의해 설정됩니다.
npm에서 타동적 종속성을 어떻게 확인할 수 있나요?
실행 npm ls --all 전체 종속성 트리를 출력하려면. 특정 패키지가 설치된 이유를 확인하려면 다음 명령을 실행하세요. npm explain <package>, 이를 통해 해당 항목으로 이어지는 모든 종속성 경로를 확인할 수 있습니다.
전달적 종속성의 취약점은 어떻게 수정하나요?
먼저, 그 위에 있는 직접 의존성을 업데이트하면 수정된 버전이 함께 설치되는지 확인하세요. 상위 패키지의 버전 범위가 수정 사항을 차단하는 경우, npm 8.3부터 지원되는 npm 오버라이드를 사용하여 패치된 버전을 강제로 적용할 수 있습니다. 다만, 이 경우 상위 패키지 유지보수자가 테스트하지 않았을 가능성이 높은 조합을 사용하게 됩니다. 패키지가 더 이상 유지보수되지 않아 수정된 버전이 없는 경우, 백포트된 수정본을 적용하거나, 직접 포크하여 패치를 적용하거나, 해당 패키지에 의존하는 다른 패키지로 대체하는 방법이 있습니다.
전이가 있는 종속성이 보안 위험이 될 수 있나요?
네. 실행되는 해당 코드들은 사용자의 빌드나 앱과 동일한 접근 권한을 얻게 되며, 일부는 설치 스크립트를 실행하기도 합니다. 또한 최상위 패키지 하나만으로도 수백 개의 코드가 유입될 수 있습니다. 2018년의 ‘이벤트 스트림(event-stream)’ 사건과 2026년의 ‘마스트라(Mastra)’ 침해 사건을 포함한 공급망 공격들은 새로 추가된 전이적 의존성을 통해 악성 페이로드를 전달했습니다.
유전적 위험이란 무엇인가요?
‘상속된 위험’이란 프로젝트가 선택한 종속성에 포함된 패키지들로부터 프로젝트가 감수하게 되는 보안 위험을 말합니다. 여기에는 전이적 종속성 트리 내 어디에 있든 취약점, 악성 코드 및 유지보수가 중단된 패키지가 모두 포함됩니다.
출처
- npm용 SecureChain, TuxCare
- 오픈 소스 소프트웨어용 SecureChain, TuxCare
- 끝없는 라이프사이클 지원, TuxCare
- 2022년 종속성 관리 현황, Endor Labs
- 기여자 계정 탈취로 인해 145개의 Mastra npm 패키지가 해킹당했다, The Hacker News
- Mastra npm 패키지 해킹, Socket
- Node.js event-stream 모듈 보안 침해, The Hacker News
- 의존성의 의존성을 재정의하는 방법, 스테판 주디스
- npm ls, npm 문서
- npm explain, npm 문서
- npm dedupe, npm 문서
- npm ci, npm 문서
- npm audit, npm 문서
- package.json 재정의, npm 문서
- package-lock.json, npm 문서
- 캐럿 범위, node-semver
