npm 공급망 침해 사태
npm install, 왜 보안 위협이 되는가
들어가며
웹 개발 공부할 때부터 프로젝트만 받으면 항상 npm install을 쳤다. 누가 시켜서, 또는 README에 써 있어서 그냥 쳤다. 솔직히 그게 정확히 뭘 하는 명령인지 깊게 생각해본 적이 없었다. "라이브러리 받아오는 거겠지" 정도. 로컬 개발에서 필수로 해야하는거 정도.
그런데 2026년 3월에 터진 Axios npm 공급망 침해 사태를 보고, 내가 아무 생각 없이 치던 그 한 줄이 사실은 남의 코드를 내 컴퓨터에서 실행시키는 행위라는 걸 처음 실감했다.
먼저, npm이 뭔지부터
이번에 정리하면서 알게 된 것들을 차근차근 풀어본다.
npm은 Node Package Manager의 약자로, 자바스크립트 세계의 거대한 라이브러리이다. 남이 잘 만들어둔 코드를 내 프로젝트에 가져다 쓸 수 있게 해준다. 예를 들어 "HTTP 요청 보내는 기능"을 직접 다 짤 필요 없이, 이미 잘 만들어진 axios 같은 "패키지"를 받아다 쓰면 된다.
그럼 npm install을 치면 무슨 일이 벌어지냐면
- 먼저
package.json이라는 파일을 본다. 여기에 "이 프로젝트가 필요로 하는 패키지 목록" 이 적혀 있다. - 그 패키지들을 npm 창고에서 다운로드한다. 그런데 여기서 끝이 아니다. 내가 받은 패키지가 또 다른 패키지에 의존하고, 그게 또 다른 걸 의존하고… 이렇게 줄줄이 딸려온다. 이걸 의존성 트리라고 부른다. 그래서 패키지 하나 받았을 뿐인데 폴더(
node_modules)에는 수십~수백 개가 깔린다. - 그리고 결정적으로, 패키지에 따라 설치 도중
postinstall이라는 스크립트가 자동으로 실행된다. "설치 끝나고 이거 실행해줘"라고 패키지가 시켜둔 명령이다. 즉 설치만 해도 남이 짠 코드가 내 컴퓨터에서 돌 수 있다는 뜻이다. 이번 사건의 핵심이 바로 여기다.
여기서 핵심 개념이 공급망 공격 이다. 단어가 좀 어려운데 풀어보면 이렇다. 물건이 공장 → 창고 → 가게 → 나에게 오는 흐름을 "공급망"이라고 하는데, 공격자가 나를 직접 노리는 대신 이 흐름의 중간(내가 믿고 쓰는 라이브러리)을 오염시키는 방식이다. 그러면 그 오염된 걸 받는 모든 사람이 한 번에 당한다. 한 명 한 명 해킹할 필요 없이, 인기 있는 패키지 하나만 오염시키면 끝나니까 공격자 입장에선 가성비가 어마어마하다.
내가 직접 조심해도 소용없다는 게 무서운 점이다. 내 코드가 아무리 깨끗해도, 내가 가져다 쓴 남의 코드가 더러우면 나도 같이 더러워진다.
무슨 일이 있었나
Axios는 자바스크립트에서 HTTP 요청 보낼 때 거의 표준처럼 쓰이는 라이브러리다. 주당 7,000만 회 이상 다운로드되는 어마어마한 패키지다. 나도 써봤다.
- 공격자는 본 공격 약 18시간 전에 악성 의존성(
plain-crypto-js)을 미리 npm에 올려뒀다. 무기를 먼저 창고에 숨겨둔 셈이다. - 2026년 3월 31일 0시 21분(UTC), 악성으로 조작된 두 버전 **
axios 1.14.1**과 **axios 0.30.4**가 npm에 올라왔다. - 악성 버전이 올라오고 단 89초 만에 첫 감염이 보안 업체 모니터링에 잡혔다. 사람이 손쓸 틈도 없는 속도였다.
- 이 악성 버전들은 약 3시간 동안 살아 있다가(대략 새벽 3시 30분경) 제거됐다.
- 짧은 시간이었는데도, 보안 연구진은 공격자 서버에 접속한 흔적을 최소 135건 탐지했다.
다운로드 수가 워낙 많은 패키지라 단 3시간이어도 영향이 컸다. 게다가 요즘은 자동 빌드가 많아서, 사람이 직접 npm install을 안 쳐도 서버가 알아서 최신 버전을 받아 깔린 경우가 많았다. 그래서 악성 버전을 "최신"으로 표시해둔 게 특히 악질이었다 — 자동으로 최신을 받게 해둔 환경이 그대로 물렸으니까.
근본 원인
흥미로운 건, 코드의 취약점이 아니라 *개발자가 먼저 당했다**는 점이다.
1. 메인테이너의 PC가 털렸다
공격자는 Axios의 핵심 관리자 를 노렸다.
메인테이너: 오픈소스 패키지를 실제로 관리·배포하는 사람. 새 버전을 올릴 권한을 가진 핵심 인물이라, 이 사람만 뚫으면 패키지 전체를 손에 넣을 수 있다.
공격 방식은 사회공학 으로 추정된다.
사회공학: 시스템의 기술적 허점이 아니라 사람의 심리를 파고드는 해킹. "급하게 확인 부탁드립니다" 같은 그럴듯한 메시지로 속여서 스스로 함정에 빠지게 만든다. 피싱 메일이 대표적이다.
이런 식으로 관리자를 속여 그 사람 PC에 RAT 악성코드를 심은 것으로 보인다.
RAT - Remote Access Trojan: 원격에서 남의 컴퓨터를 마음대로 조종할 수 있게 해주는 악성코드. 화면 보기, 파일 빼내기, 명령 실행 등 사실상 그 PC의 주인 행세를 할 수 있다.
2. npm 계정을 탈취당했다
관리자 PC를 장악한 공격자는 거기서 npm 계정 정보를 빼냈다. 그리고 그 계정으로 정식 Axios 패키지의 악성 버전을 직접 배포했다. 사용자 입장에선 "공식 패키지의 정상 업데이트"처럼 보이니 의심할 도리가 없다. 실제로 침해 정황으로, 패키지에 등록된 관리자 이메일이 슬쩍 바뀌어 있었고(원래 주소 → 공격자 주소), 평소 쓰던 안전한 자동 배포 방식 대신 수동으로 직접 올린 흔적이 발견됐다.
여기서 충격적이었던 디테일. 그 관리자는 2단계 인증을 켜둔 상태였다. 사고 후 본인이 "나는 거의 모든 곳에 2FA/MFA를 켜놨는데 어떻게 이런 일이 일어났는지 모르겠다"고 했을 정도다.
2FA: 비밀번호 말고도 휴대폰 인증 같은 두 번째 관문을 하나 더 두는 보안 방식. 비번이 털려도 한 번 더 막아준다.
그럼 2FA가 있는데 어떻게 뚫렸을까? 핵심은 장기 토큰 이었다. 토큰은 일종의 자동 로그인 출입증인데, 자동배포 같은 데서 쓰려고 발급해두는 경우가 많다. 문제는 이 출입증이 한 번 발급되면 오래 유효하고, 들어갈 때 2FA를 다시 안 묻는다는 점이다. 공격자는 PC에서 바로 이 토큰을 훔쳐서, 비밀번호도 2FA도 건너뛰고 곧장 들어간 것으로 분석됐다. (실제로 axios는 OIDC라는 안전한 자동 배포 방식을 갖춰뒀는데, 환경변수에 토큰이 같이 있으면 npm이 그 토큰을 우선 쓰는 허점이 겹쳤다.)
여기서 큰 교훈을 얻었다. 2FA를 켰다고 끝이 아니다. 정문(비번+2FA)을 아무리 단단히 잠가도, 옆에 난 쪽문(오래된 토큰)을 안 잠그면 거기로 들어온다.
3. 악성 의존성 + postinstall
악성 버전에는 plain-crypto-js 라는 새 의존성 하나가 슬쩍 추가돼 있었다. 이름만 보면 암호화 관련 정상 패키지 같지만, 공격용으로 새로 만들어진 가짜였다. 원래 axios에는 없던, 그리고 axios 코드가 실제로 쓰지도 않는 "유령 의존성"이었다. 진짜 axios 기능과는 아무 상관없이, 오직 악성코드를 실어 나르려고 끼워 넣은 것이다.
이 패키지의 postinstall 스크립트가 진짜 무기였다. 그냥 코드도 아니고, 분석을 어렵게 하려고 이중으로 난독화까지 돼 있었다.
난독화: 코드를 일부러 알아보기 어렵게 꼬아놓는 것. 이번엔 글자를 거꾸로 뒤집고(Base64를 뒤집고), 거기에 또 다른 암호(XOR)를 덧씌워서, 사람이 슥 봐서는 "이게 악성인지" 알 수 없게 만들었다.
이 스크립트가 npm install 도중 자동 실행되면서 벌어진 일:
- 공격자 서버(C2)인
sfrclak[.]com:8000에 몰래 접속한다. - 피해자의 운영체제를 확인하고, 거기에 딱 맞는 2단계 RAT 악성코드를 내려받아 실행한다.
- 그러고는 자기 흔적을 지운다. 설치 스크립트 자신을 삭제하고,
package.json을 깨끗한 가짜로 바꿔치기해서 "원래부터 멀쩡했던 것처럼" 위장한다.
C2 [명령 제어 서버]: 감염된 컴퓨터들이 공격자와 연락을 주고받는 본부 서버. 여기서 명령을 받고, 훔친 정보를 여기로 보낸다.
심지어 공격자는 이 본부 서버로 보내는 통신을 packages.npm.org 같은 주소로 위장했다. 언뜻 보면 진짜 npm 서버로 가는 정상 트래픽처럼 보이게 해서, 감시망을 피하려 한 것이다.
4. OS별 맞춤 악성코드
특히 무서웠던 건, 이 공격이 운영체제를 가리지 않았다는 점이다. 피해자 OS를 확인하고 각각에 맞는 RAT를 심었다.
- 윈도우 — PowerShell 기반 RAT를 심는데, 보안 프로그램의 감시를 피하려고 윈도우 기본 프로그램을 다른 이름으로 복사해서 실행했다. 게다가 재부팅해도 다시 살아나도록 자리를 잡았다.
- macOS — 시스템 캐시 폴더 깊숙한 곳(
/Library/Caches/...)에 실행 파일을 숨기고, 애플 정상 프로세스인 척 위장했다. 여기도 로그인할 때마다 되살아나게 했다. - 리눅스 — 임시 폴더(
/tmp/)에 파이썬 스크립트를 떨궈 실행했다. 리눅스는 주로 자동 빌드 서버라 굳이 재부팅 생존 장치는 안 뒀다.
지속(persistence): 악성코드가 컴퓨터를 껐다 켜도 다시 살아나도록 자리를 잡는 것. 한 번 감염되면 재부팅만으로는 안 사라진다는 뜻이다.
감염된 RAT는 60초마다 본부에 정상 신호 를 보내면서, 컴퓨터 이름·실행 중인 프로그램 목록·폴더 정보 등을 함께 넘겼다. 이건 전형적인 정찰 행동으로, 보통 그다음 단계인 비밀번호·인증정보 훔치기로 이어진다. 흥미로운 디테일 하나 — 이 통신에 쓰인 흔적(아주 옛날 인터넷 익스플로러로 위장한 식별값 등)이 부자연스러워서, 오히려 보안 업체들이 감염을 잡아내는 단서가 됐다고 한다.
즉 누군가 npm install로 그 버전을 설치하는 순간, 본인 컴퓨터가 통째로 공격자에게 넘어가는 구조였다. 내가 아무 생각 없이 치던 그 명령이, 이 경우엔 곧장 악성코드 실행 버튼이었던 셈이다.
어떻게 대응했나
- npm과 Axios 팀이 악성 버전을 약 3시간 만에 제거했다.
- 영향받은 사용자에게 권고된 조치:
- 안전한 버전으로 되돌리기 —
axios 1.14.0같은 정상 버전으로.package.json의overrides기능으로 버전을 강제 고정하는 방법도 안내됐다. node_modules안의plain-crypto-js를 지우고 다시 설치.- 노출됐을 수 있는 모든 비밀번호·토큰·API 키 즉시 교체 — 보안에선 이걸 "회전(rotate)"이라고 한다. 어차피 한 번 새어나갔을 수 있으니 통째로 새것으로 바꾸는 것이다.
- 시스템에서 침해 흔적 점검 — 예를 들어
sfrclak[.]com(공격자 C2 서버) 접속 기록이 있는지, 위에서 말한 의심 파일 경로에 뭔가 있는지. - 만약 RAT 흔적이 발견됐다면, 그 컴퓨터는 이미 완전히 장악됐다고 보고 아예 깨끗하게 재설치하라고 권고됐다. 악성코드는 어디 또 숨어 있을지 모르니, 부분 삭제보다 갈아엎는 게 확실하기 때문이다.
- 안전한 버전으로 되돌리기 —
이런 일이 처음이 아니다
이번 사건을 계기로 찾아보니, npm 공급망 공격은 예전부터 꾸준히 반복돼 온 패턴이었다.
- event-stream (2018) — 매주 수백만 번 다운로드되던 인기 패키지. 공격자가 기존 관리자에게 접근해 관리 권한을 슬쩍 넘겨받은 뒤, 비트코인 지갑을 노리는 악성코드를 심었다. --.
- ua-parser-js (2021) — 수천만 다운로드급 패키지. 계정이 탈취돼 악성 버전 3개가 같은 주에 올라왔고, 암호화폐 채굴기와 비밀번호 탈취 악성코드가 같이 깔렸다.
- node-ipc (2022) — 관리자가 정치적 의사 표시를 위해 일부러 코드에 특정 지역 사용자의 파일을 망가뜨리는 로직을 넣은 사건.
패턴이 비슷하다. 인기 많은 패키지 + 관리자 계정 탈취(또는 변심) + 설치 시 자동 실행되는 악성코드.
실질 방어법
앞으로 npm install을 계속 칠 사람으로서, 찾아본 현실적인 방어법은
- 버전을 고정하고
npm ci를 쓰자.npm install대신npm ci를 쓰면,package-lock.json에 적힌 정확히 그 버전만 설치한다. 멋대로 최신 버전(악성일 수도 있는)을 받아오지 않는다. 잠금 파일(lockfile)은 꼭 깃에 같이 올려두자. - 새 버전은 좀 묵혔다 쓰자. 방금 올라온 따끈한 버전을 바로 받으면, 악성 버전이 올라온 순간 그대로 물린다(이번 사건이 정확히 그랬다). "나온 지 며칠 지난 버전만 설치" 같은 규칙(min-release-age)을 두면, 그사이 누군가 악성 버전을 발견해줄 시간이 생긴다. 한 보안업체는 21일 이내 버전은 쓰지 말라고까지 권한다.
- 꼭 필요할 땐 설치 스크립트를 끄자.
npm install --ignore-scripts를 쓰면postinstall같은 자동 실행 스크립트를 막을 수 있다. 이번 공격의 핵심 통로가 바로 그 스크립트였다. npm audit로 점검하자. 내 의존성 중에 알려진 위험이 있는지 검사해주는 기본 명령이다. 완벽하진 않지만 안 하는 것보단 훨씬 낫다.- 토큰을 오래 방치하지 말자. 앞서 봤듯이 공격의 결정타는 오래된 장기 토큰이었다. 안 쓰는 토큰은 지우고, 꼭 필요한 것만 짧게 유지하는 게 좋다.
완벽한 방법은 없다. 하지만 이런 습관들이 쌓이면, 적어도 "방금 올라온 악성 버전을 자동으로 받아서 즉시 감염" 되는 최악은 상당히 피할 수 있다는 걸 알게 됐다. 실제로 잠금 파일을 쓰거나 설치 스크립트를 꺼둔 팀들은 이번 공격을 그냥 비껴갔다.
보완이 미흡했던 부분 / 우리가 배울 점
이건 특정 회사 잘못이라기보다, 생태계 전체의 구조적 약점을 보여준 사건이다.
- 사람이 가장 약한 고리 — 코드가 아무리 튼튼해도, 관리자 한 명이 사회공학에 당하면 끝이다. 보안의 시작은 결국 사람이라는 걸 배웠다.
- 2FA만으론 부족하다 — 2단계 인증을 켜뒀는데도 오래된 토큰 하나로 뚫렸다. 보안은 정문만 잠근다고 끝이 아니라, 토큰이나 예전 권한까지 다 챙겨야 한다.
postinstall의 위험성 — 설치만 해도 코드가 자동 실행된다는 게 편리하면서도 무섭다. 그래서--ignore-scripts같은 옵션이 왜 있는지 이제 이해된다.- 버전 고정의 중요성 — 항상 최신 버전을 자동으로 받게 해두면, 악성 버전이 올라온 순간 바로 물릴 수 있다. 잠금 파일로 버전을 고정하고, 새 버전은 좀 묵혀서 검증된 뒤 올리는 습관이 필요하다.
- 내가 의존하는 게 수백 개 —
npm install한 번에 딸려오는 패키지가 수십~수백 개다. 그중 하나만 오염돼도 나까지 위험해진다. 내 코드만 안전하다고 끝이 아니었다.
마무리하며 느낀 점
정리하면서 머리에 남은 키워드들:
- 관리자 공격 — 나 -> axios -> 관리자, 이 사슬의 가장 약한 고리 하나가 끊기니 사슬 끝에 매달린 일반 사용자까지 흔들렸다.
- 사회 공학 — 코드가 아니라 사람을 속여서 시작된 공격이었다. 아무리 시스템을 단단히 짜도, 결국 사람이 뚫리면 무너진다.
- 자동화의 양면 — 자동 빌드·자동 업데이트는 편하지만, 악성 버전도 똑같이 자동으로 빠르게 퍼뜨린다. 편할수록 안전장치를 같이 둬야 한다.
시키니까 치던 한 줄에서 시작해, npm이 뭔지·의존성이 뭔지·postinstall이 뭔지·공급망 공격이 뭔지, 그리고 어떻게 막는지까지 알게 됐다. 보안 입문자로서 "편리함 뒤에는 항상 신뢰의 비용이 숨어 있다" 는 걸 가장 와닿게 배운 사례였다. 앞으로 npm install을 칠 때, 적어도 한 번은 "이거 믿어도 되나?" 하고 생각하게 될 것 같다.