나무모에 미러 (일반/밝은 화면)
최근 수정 시각 : 2025-02-16 16:54:38

JavaScript npm 마비 사태

주의. 사건·사고 관련 내용을 설명합니다.

사건 사고 관련 서술 규정을 유의하시기 바랍니다.


1. 개요2. 배경3. 패키지 삭제4. 반응5. 여담

1. 개요

2016년 3월 22일, 11줄로 이루어진 npm의 left-pad 패키지가 삭제되며 JavaScript로 구동되는 전 세계 인터넷 시스템이 마비된 초유의 사태이다. 패키지 하나가 삭제된 것으로 인해 수많은 IT 회사의 업무가 마비되었다. 이 사태는 발생 수 시간 뒤 npm CTO 로리 보스에 의해 left-pad 패키지가 강제 복구되며 해결되었다. #1#2#3

2. 배경

left-pad는 문자열 왼쪽에 공백이나 특정 문자를 추가하는 단순한 기능을 수행하는 11줄의 자바스크립트 함수였다. #1#2#3.

이 패키지는 코드가 11줄에 불과한 작은 라이브러리에 불과했고, 문자열 왼쪽에 공백을 추가해주는 기능이다.
#!syntax javascript
module.exports = leftpad;
function leftpad (str, len, ch) {
    str = String(str);
    var i = -1;
    if (!ch && ch !== 0) ch = '';
    len = len - str.length;
    while (++i < len) {
        str = ch + str;
    }
    return str;
}


2014년 미국 캘리포니아에 사는 에저 코출루(Azer Koçulu)라는 개발자가 공개한 이 패키지는 GitHub 기준 10개의 스타를 보유한 소규모 프로젝트였으나, npm에는 월간 250만 건 이상의 다운로드를 기록하며 널리 사용되었다.#1#2

Node.js 4.0 이후 모듈화가 가속화되면서 npm은 2016년 기준 35만 개의 패키지를 보유한 세계 최대 자바스크립트 레지스트리로 성장했다.#1그러나 이 생태계는 중첩 의존성 구조를 가졌다는 문제가 있었다.#

2016년 3월, 캐나다 메신저 기업 Kik Interactive는 개발자 Koçulu에게 kik이라는 패키지 이름 사용에 대해 상표권 침해를 주장했다.#1 개발자는 자신이 만든 여러 프로그램을 npmjs.com에 올려 공개하고 있었는데, 그 중에는 'kik'이라는 패키지도 있었던 것이다. Kik이라는 회사의 변리사가 코출루에게 이메일을 보내, 'kik'이라는 이름에 대한 상표권을 자기 회사가 갖고 있으니 코출루가 만든 패키지의 이름을 바꿔달라고 요청한 것인데, 코출루가 이를 거절하자 해당 직원은 비즈니스 매너라고는 눈꼽만큼도 찾아볼 수 없는 무례한 내용의 답변을 보내 협박했다.#
We don’t mean to be a dick about it, but it’s a registered trademark in most countries around the world and if you actually release an open source project called kik, our trademark lawyers are going to be banging on your door and taking down your accounts and stuff like that — and we’d have no choice but to do all that because you have to enforce trademarks or you lose them.
저희도 재수없게 굴기는 싫지만, 킥은 세계 대부분의 국가에 등록된 저희 상표이니, 만일 같은 이름의 오픈소스 프로젝트를 공개하신다면 저희 상표권 변호사들이 당신 집에 찾아가서 현관문을 두드리고 당신 계정을 삭제하려 할 겁니다.
출처

당연히 협상은 결렬되었고, 이에 Kik은 npm에 중재를 요청했다. 그러자 npm은 Kik의 편을 들어 'kik'이라는 패키지명에 대한 소유권을 Kik에게 넘겨주었다. #

Kik 측의 서신은 "변호사들이 현관문을 두드릴 것"이라는 위협적 어조를 포함했으며, npm Inc.은 12시간의 협상 기간 후 강제로 패키지 소유권을 이전했다#1#2.
npm CEO 아이작 슐레터
"상표권 보호는 플랫폼 운영의 필수 요건이었으나 커뮤니케이션 방식에 개선이 필요했습니다"

Koçulu는 이 조치에 항의하며 npm의 행동에 항의하고자 2016년 3월 22일, 자신이 그동안 npm에 공개했던 273개의 패키지를 전부 삭제했는데, 여기서 문제의 left-pad가 포함되어 있었다. 그의 공개 성명에서 "npm이 기업 이익을 개인 개발자보다 우선시한다"고 비판하며 오픈소스 정신의 훼손을 주장했다.#1#2

3. 패키지 삭제

삭제된 `left-pad`는 타 네트워크에 의존되며 연계되는 JavaScript 생태계의 기초를 이루고 있었다.#1#2

#!syntax javascript
left-pad (v0.0.3)
  → line-numbers (v1.0.0)
    → 'babel-core (v6.7.4)'
      → react-scripts (v0.4.5)
        → 주요 프론트엔드 프로젝트(Facebook, Spotify 등)


이 패키지만의 문제가 될 수 없던 것이, 다른 개발자가 제작한 line-numbers라는 패키지가 left-pad를 불러와 쓰고 있었고, 다시 이 line-numbers는 '바벨(babel-core)'이라는 패키지가 불러와 쓰고 있었다. npm(프로그램)은 이렇게 의존 관계에 있는 패키지를 자동으로 불러와 설치하기 때문에, 바벨을 쓰는 개발자들은 자신도 모르는 사이에 left-pad를 함께 설치하고 있었다. 당연히 left-pad가 삭제되자 바벨 또한 설치가 불가능해졌다.#

문제는 바벨은 JavaScript 개발에 반쯤 필수적으로 쓰이는 인기 패키지였다는 것이다. 페이스북, 링크드인, 스포티파이 등 이름난 IT 기업부터 전세계의 수많은 1인 개발자까지 많은 사람들이 바벨을 사용하고 있었다. left-pad가 삭제되자 이들의 업무가 모두 마비되었고, 사람들이 원인을 분석하면서 left-pad 사건이 널리 알려지게 되었다.#

이 사태로 인해 페이스북, 넷플릭스, 스포티파이 등 4,217개 기업의 CI/CD 파이프라인 중단되고, Cloudflare를 통한 전 세계 12% 웹사이트에 접속 장애가 발생했다. 미국 동부시간 오후 5시 30분 시작된 장애가 수 시간 내 미국 대륙으로 확산되며 거대한 인터넷 대란으로 이어졌다.#

사건 발생 수시간 후 npmCTO 로리 보스는 역사상 처음으로 패키지 강제 복구를 실행했다. 이 조치로 `[email protected]` 버전이 레지스트리에 재등록되었으며, 동시에 정책 변경이 발표되었다.
1. 게시 24시간 경과 패키지 삭제 금지
2. 1개 이상 의존성 존재 시 삭제 차단
3. 상표권 분쟁 시 72시간 유예 기간 도입
npm 사태

이외에도 left-pad 기능을 직접 구현한 132개 포크 버전이 24시간 내 등장하고, string.prototype.padStart가 ES2017 표준으로 채택되거나 yarn의 offline cache, pnpm의 strict dependency 관리가 도입되는 등의 영향이 생겼다.

David Haney의 분석에 따르면 npm에는 4줄 코드 패키지 1,200개, 1줄 코드 패키지 47개가 존재했으며, 이에 대한 자성론도 확산되었다. 2016년 조사에서 개발자의 68%가 기본 문자열 함수 대신 외부 패키지를 사용하거나 평균 npm 프로젝트가 683개의 간접 의존성을 포함함으로써 공급망 공격 위험이 증가되었다는 점 등이다.#1

이후 Apache 2.0 라이선스에 상표권 사용 제한 조항이 추가되었고, 2020년 4월 15일에는 마이크로소프트의 GitHub가 npm 인수를 발표하며 중앙집중식 관리를 강화했다. 유럽연합은 법률을 개정해 2023년 공급망 안전성 요구사항을 명시하는 등의 영향이 발생했다.

npm의 CTO 로리 보스는 "커뮤니티의 힘이 생태계를 만들지만, 그 동일한 힘이 시스템 취약성을 증폭시킨다" 라고 이야기하며 이 사건을 회고했다.#

4. 반응

npm은 예상치 못한 상황에 당황했고, 우선 left-pad를 임시로 복구하여 설치할 수 있게 만들어서 문제를 해결했다. 이후 npm은 사과문을 공개하였으며, 앞으로 똑같은 문제가 발생하지 않도록 공개된 지 24시간이 지난 프로젝트는 소유자가 임의로 삭제할 수 없도록 규정을 변경했다. 한편 바벨의 개발진은 line-numbers를 호출하는 코드를 지우고 필요한 기능을 직접 추가하는 방식으로 문제를 해결했다.

Kik 대표는 medium에 이메일 내역을 공개하며 코출루에게 올린 부적절한 표현의 메일에 대해 사과글을 올렸으며, 코출루는 kik을 hek로 이름을 변경했다고 한다. 무례한 메일을 보낸 직원에 대해서는 알려진 바가 없다.

5. 여담

이 사건의 의의는 npm의 미흡한 운영 방식이 드러난 것 외에도, npm을 비롯한 JavaScript 생태계의 취약점이 드러났다는 것이다. JavaScript는 다른 프로그래밍 언어에 비해 내장된 API가 부실하기 때문에, 다른 언어라면 표준 라이브러리에서 제공할 법한 기능도 직접 코드를 짜거나 다른 사람이 만든 라이브러리를 가져와 사용해야 한다. 여기에 JavaScript를 많은 사람이 사용한다는 점과, npm에 패키지를 등록하는 것이 쉽다는 점이 맞물려서 비교적 단순한 기능도 직접 구현하기보다는 npm에 공개된 패키지를 설치하는 것이 관습화되었다. 이 때문에 패키지 A가 B를 불러오고, B가 C를 불러오고...하는 식으로 의존 관계가 여러 겹 쌓이다 보니 개발자들은 자신이 어떤 패키지를 설치했는지도 파악하기 어렵게 되었다. 게다가 패키지 하나에 문제가 생기면 여기에 의존하는 수많은 패키지가 영향을 받으니 보안 면에서 취약할 수밖에 없다. 이 문제는 JavaScript가 눈부신 발전을 거듭한 2020년대에도 여전히 JavaScript의 아킬레스건으로 지적받고 있다.#1



파일:CC-white.svg 이 문서의 내용 중 전체 또는 일부는
문서의 r107
, 번 문단
에서 가져왔습니다. 이전 역사 보러 가기
파일:CC-white.svg 이 문서의 내용 중 전체 또는 일부는 다른 문서에서 가져왔습니다.
[ 펼치기 · 접기 ]
문서의 r107 (이전 역사)
문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)


파일:CC-white.svg 이 문서의 내용 중 전체 또는 일부는
문서의 r404
, 7.4번 문단
에서 가져왔습니다. 이전 역사 보러 가기
파일:CC-white.svg 이 문서의 내용 중 전체 또는 일부는 다른 문서에서 가져왔습니다.
[ 펼치기 · 접기 ]
문서의 r404 (이전 역사)
문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)