Workspace IndexKnowledge Notes › A dependency is not a file, it is authority you handed to someone else — Parity 2017 and event-stream 2018

#78PoC2026-09-18file

A dependency is not a file, it is authority you handed to someone else — Parity 2017 and event-stream 2018

Two incidents a year apart broke no cryptography and still cost more than most exploits. On 2017-11-06 a GitHub user, devops199, called an initialisation function on the shared library behind every Parity multisig wallet, became its owner because the library had never been initialised, and ran `selfdestruct`. About 587 wallets that delegated their logic to that address were left pointing at empty code; roughly 513,774 ETH were frozen permanently, not stolen, because the code they needed no longer existed. On 2018-09-09 a new maintainer of event-stream, an npm package with about two million weekly downloads, added a dependency called flatmap-stream; version 0.1.1 of it carried an encrypted payload that activated only inside Copay, a Bitcoin wallet app, and exfiltrated seed phrases from wallets holding more than 100 BTC or 1,000 BCH. It was found on 2018-11-20, after Copay had shipped it in versions 5.0.2 to 5.1.0. The original maintainer had handed the package over because he no longer used it and someone offered to help.

The common surface is not a vulnerability class. It is that code you depend on is maintained by someone else, and maintenance includes the right to change it. Parity's wallets trusted a library address; event-stream's users trusted a name on a registry. In both cases the trust was to a party, not to a file, and the party changed, by accident in one case and by handover in the other. Three mistakes repeat: treating a dependency as a fixed object; equating "widely used" with "reviewed"; and auditing once without pinning, which means the audit described one commit and the deployment ran another. For Jayverse the checklist is short and applies today because of the S&P and OpenZeppelin news: exact version pins and committed lockfiles, everywhere; count the dependencies that can move money and read at least those; and keep "the code is safe" and "the supply chain is safe" as two separate statements that fail for different reasons. Status on 2026-09-18: our Solidity pins are submodule commits, which is exact; one CI workflow freezes its lockfile; the rest is unverified.

Why

Auditors and reviewers look at code. Both of these failures were invisible in the code at review time. Parity's library was fine as written; the failure was that a deployment step, initialising the library, had not happened, and the library's authority over hundreds of wallets was therefore unclaimed. event-stream was fine when Copay's engineers reviewed it; the failure arrived later through a version bump nobody read, because the package was popular and popularity felt like review. A dependency is a relationship in time. What you audited is a snapshot; what you run is whatever the relationship delivers next.

This is why the S&P acquisition of OpenZeppelin belongs next to these two cases even though nothing bad happened. Every Jayverse contract that imports OpenZeppelin has a relationship with whoever maintains it, and on 2026-09-17 that party changed. The code did not. The right response is the same as it would have been the day before: know exactly which commit you run, know who can change what you will run next, and make the change a decision rather than an event.

How it works

The two cases side by side

Parity multisig, 2017-11event-stream, 2018-09 to 11
What was depended ona shared library contract at one address, reached by delegatecallan npm package by name, resolved by semver at install
What changedanyone could call initWallet on the uninitialised library, become owner, and selfdestruct itownership of the package passed to a new maintainer who added a malicious dependency
Who did itdevops199, apparently by accident: "I accidentally killed it"right9ctrl, deliberately, targeting one wallet app
Damageabout 587 wallets bricked; about 513,774 ETH frozen (roughly $150 million then), never recoveredCopay 5.0.2 to 5.1.0 shipped a seed-phrase stealer for large wallets; scale of theft unclear
Cryptography brokennonenone
Would an audit have caught itof the library code, no; of the deployment, yesof event-stream 3.3.5, no; of the 3.3.6 bump, only if someone read it

The three mistakes

  1. A dependency is a fixed object. It is an address or a name that resolves to different code over time. Pin the resolution: a commit hash, an exact version, a lockfile committed and enforced.
  2. Widely used means reviewed. Two million downloads a week is a measure of trust, not of reading. Popularity increases the payoff for an attacker and the diffusion of responsibility for defenders.
  3. Audit once, pin never. An audit is a statement about one commit. If the deployment can drift from that commit without a decision, the audit is decoration.

What "pinned" means in each stack

StackLooseExact
Foundry / Solidityremapping to a branch; forge install without a taggit submodule at a commit, recorded in .gitmodules and the superproject
npm / pnpm^5.1.0 in package.json and no lockfile in CIlockfile committed and pnpm install --frozen-lockfile in CI, so a mismatch fails the build
Contracts on chaindelegatecall to an upgradeable or uninitialised libraryimmutable library address, initialisation checked in the constructor or deployment script, ownership asserted in a test

Where it lands in Jayverse

  • Solidity: already exact, keep it so. jayverse-token and jayverse-personas pin OpenZeppelin v5.1.0 as submodule commits; Verex inherits v4.7.0 through ctf-exchange. A bump is a PR that names the release notes it read.
  • JavaScript: enforce the lockfile. One CI workflow uses --frozen-lockfile today. Every app that touches keys or money, wallet, rabbit, verex api, should fail its build on a lockfile mismatch. Five minutes each.
  • Count the money-moving dependencies. For each repo, list the packages and libraries on the path between a user action and a transfer. It is usually four or five. Read those, and only those, at every bump.
  • Two statements, two owners. "The code is safe" is the auditor's statement about a commit. "The supply chain is safe" is ours about pins, CI, and who has publish rights. Write both into the permission table the S&P item asks for.

Verified and unverified

Verified on 2026-09-18 from the incident record (Parity post-mortem coverage, TechCrunch 2017-11-07; npm's and Snyk's event-stream post-mortems): dates, the uninitialised library and selfdestruct mechanism, the flatmap-stream payload and Copay targeting, the affected Copay versions, the maintainer handover. Figures reported with small variance across sources: 584 to 587 wallets; 513,774 ETH; "about two million" weekly downloads. Jayverse state from the repos on this machine: submodule versions, import counts, one workflow with --frozen-lockfile. Not verified: how much was stolen through the Copay backdoor; whether any Jayverse app other than that one workflow enforces its lockfile.

Sources: TechCrunch — A major vulnerability has frozen hundreds of millions of dollars of Ethereum, 2017-11-07 · Parity post-mortem coverage — Coin Bureau · npm blog — Details about the event-stream incident, 2018-11 · Snyk — A post-mortem of the malicious event-stream backdoor

Key expressions

Words and phrases from this page worth keeping, with the Korean meaning and the sentence they come from.

Expression뜻 · 쓰이는 자리
bricked(기기·지갑이) 먹통이 되다, 못 쓰게 되다 · 소프트웨어 결함으로 자산에 접근 불가능해질 때. "about 587 wallets bricked"
hand over넘겨주다, 인계하다 · 패키지·프로젝트 관리권을 다른 사람에게 넘길 때. "had handed the package over"
drift from(기준·원본에서) 조금씩 벗어나다 · 배포된 코드가 감사받은 커밋과 달라질 때. "can drift from that commit without a decision"
decoration장식(에 불과한 것), 형식적인 것 · 실질적 효력이 없는 문서·절차를 비판할 때. "the audit is decoration"
pin (v.)(버전을) 고정하다 · 의존성 버전이 임의로 바뀌지 않게 못박을 때. "Pin the resolution"
diffusion of responsibility책임 분산(다들 남이 확인했겠지 하는 심리) · 인기 많은 패키지라서 아무도 안 읽고 넘어갈 때. "the diffusion of responsibility for defenders"
fail the build빌드를 실패시키다 · CI에서 조건 불일치 시 배포를 막을 때. "a mismatch fails the build"
a relationship in time시간에 걸친 관계 · 의존성을 한번 고정된 것이 아니라 계속 변하는 관계로 볼 때. "A dependency is a relationship in time"
BCH비트코인캐시(Bitcoin Cash) · 비트코인에서 하드포크된 알트코인 티커, 대량 보유 지갑이 탈취 표적이 된 자산 중 하나. "wallets holding more than 100 BTC or 1,000 BCH"
S&PS&P 글로벌(S&P Global) · 신용평가·금융데이터 대기업, OpenZeppelin을 인수해 감사 주체가 바뀌었음을 보여주는 사례로 언급됨. "the S&P acquisition of OpenZeppelin belongs next to these two cases"
Copay비트코인 지갑 앱(Bitcoin wallet application) · event-stream 악성코드가 실제로 표적으로 삼은 지갑 소프트웨어, 대형 지갑의 시드구문이 탈취됨. "activated only inside Copay, a Bitcoin wallet app"

← All Knowledge Notes · Workspace Index · Top ↑

의존성은 파일이 아니라 남에게 준 권한이다 — Parity 2017과 event-stream 2018

1년 간격의 두 사고는 암호를 하나도 깨지 않았고 그래도 대부분의 익스플로잇보다 큰 비용을 냈습니다. 2017-11-06 GitHub 사용자 devops199가 모든 Parity 멀티시그 지갑 뒤의 공용 라이브러리에서 초기화 함수를 호출해, 라이브러리가 초기화된 적이 없었기 때문에 소유자가 되고, `selfdestruct`를 실행했습니다. 그 주소에 로직을 위임하던 약 587개 지갑이 빈 코드를 가리키게 됐고, 약 513,774 ETH가 영구 동결됐습니다. 도난이 아니라, 필요한 코드가 더는 존재하지 않게 된 것입니다. 2018-09-09 주간 약 2백만 다운로드의 npm 패키지 event-stream의 새 유지자가 flatmap-stream이라는 의존성을 추가했습니다. 그 0.1.1 버전은 비트코인 지갑 앱 Copay 안에서만 활성화되는 암호화된 페이로드를 담고 있었고, 100 BTC 또는 1,000 BCH 이상을 보유한 지갑의 시드 문구를 빼냈습니다. 2018-11-20에 발견됐고, 그 전에 Copay가 5.0.2에서 5.1.0 버전으로 배포했습니다. 원 유지자는 더는 쓰지 않는 패키지였고 누군가 돕겠다고 해서 넘겼습니다.

공통 표면은 취약점 부류가 아닙니다. 내가 의존하는 코드를 남이 유지하고, 유지에는 바꿀 권리가 포함된다는 것입니다. Parity 지갑은 라이브러리 주소를 신뢰했고, event-stream 사용자는 레지스트리의 이름을 신뢰했습니다. 두 경우 모두 신뢰는 파일이 아니라 상대에게 준 것이었고, 상대가 바뀌었습니다. 하나는 사고로, 하나는 이관으로. 세 실수가 반복됩니다. 의존성을 고정된 물체로 취급하는 것, "널리 쓰임"을 "검토됨"과 동일시하는 것, 핀 없이 한 번 감사하는 것. 마지막은 감사가 커밋 하나를 서술했는데 배포는 다른 커밋을 돌렸다는 뜻입니다. Jayverse의 체크리스트는 짧고, S&P와 OpenZeppelin 뉴스 때문에 오늘 적용됩니다. 정확한 버전 핀과 커밋된 락파일, 모든 곳에; 돈을 움직일 수 있는 의존성을 세고 최소한 그것들은 읽기; "코드가 안전"과 "공급망이 안전"을 다른 이유로 깨지는 두 개의 별도 진술로 유지하기. 2026-09-18 기준 상태: 우리 Solidity 핀은 서브모듈 커밋이라 정확함; CI 워크플로 하나가 락파일을 고정; 나머지는 미확인.

감사자와 리뷰어는 코드를 봅니다. 두 실패 모두 리뷰 시점의 코드에서는 보이지 않았습니다. Parity 라이브러리는 쓰인 대로는 괜찮았고, 실패는 배포 단계, 라이브러리 초기화가 일어나지 않았다는 것이었으며, 따라서 수백 개 지갑에 대한 라이브러리의 권한이 주인 없이 남아 있었습니다. event-stream은 Copay 엔지니어가 검토했을 때 괜찮았고, 실패는 나중에 아무도 읽지 않은 버전 올림으로 도착했습니다. 패키지가 인기 있었고 인기가 검토처럼 느껴졌기 때문입니다. 의존성은 시간 속의 관계입니다. 감사한 것은 스냅샷이고, 돌리는 것은 그 관계가 다음에 전달하는 무엇입니다.

그래서 나쁜 일이 아무것도 일어나지 않았는데도 S&P의 OpenZeppelin 인수가 이 두 사례 옆에 놓입니다. OpenZeppelin을 임포트하는 모든 Jayverse 컨트랙트는 그것을 유지하는 누군가와 관계가 있고, 2026-09-17에 그 상대가 바뀌었습니다. 코드는 바뀌지 않았습니다. 올바른 대응은 전날에도 같았을 것입니다. 정확히 어느 커밋을 돌리는지 알고, 다음에 돌릴 것을 누가 바꿀 수 있는지 알고, 그 변경을 사건이 아니라 결정으로 만드는 것.

동작 방식

두 사례 나란히

Parity 멀티시그, 2017-11event-stream, 2018-09~11
의존한 것delegatecall로 닿는 한 주소의 공용 라이브러리 컨트랙트설치 시 semver로 해석되는 이름의 npm 패키지
바뀐 것누구나 초기화되지 않은 라이브러리의 initWallet을 호출해 소유자가 되고 selfdestruct할 수 있었음패키지 소유권이 새 유지자로 넘어가 악성 의존성이 추가됨
누가devops199, 사고로 보임: "I accidentally killed it"right9ctrl, 의도적으로, 지갑 앱 하나를 겨냥
피해약 587개 지갑 벽돌; 약 513,774 ETH 동결(당시 약 1.5억 달러), 복구 없음Copay 5.0.2~5.1.0이 대형 지갑용 시드 탈취기를 배포; 도난 규모 불명
깨진 암호없음없음
감사가 잡았을까라이브러리 코드는 아니오; 배포는 예event-stream 3.3.5는 아니오; 3.3.6 올림은 누가 읽었다면

세 실수

  1. 의존성은 고정된 물체다. 사실은 시간에 따라 다른 코드로 해석되는 주소나 이름이다. 해석을 고정할 것: 커밋 해시, 정확한 버전, 커밋되고 강제되는 락파일.
  2. 널리 쓰이면 검토됐다. 주간 2백만 다운로드는 신뢰의 척도이지 읽음의 척도가 아니다. 인기는 공격자의 보수를 키우고 방어자의 책임을 분산시킨다.
  3. 한 번 감사, 핀은 없음. 감사는 커밋 하나에 대한 진술이다. 배포가 결정 없이 그 커밋에서 벗어날 수 있으면 감사는 장식이다.

각 스택에서 "핀됨"의 뜻

스택느슨함정확함
Foundry / Solidity브랜치로의 리매핑; 태그 없는 forge install.gitmodules와 상위 프로젝트에 기록된 커밋의 git 서브모듈
npm / pnpmpackage.json의 ^5.1.0과 CI에 락파일 없음락파일 커밋과 CI의 pnpm install --frozen-lockfile, 불일치 시 빌드 실패
온체인 컨트랙트업그레이드 가능하거나 초기화되지 않은 라이브러리로의 delegatecall불변 라이브러리 주소, 생성자나 배포 스크립트에서 초기화 확인, 테스트에서 소유권 검증

Jayverse에서의 위치

  • Solidity: 이미 정확함, 그대로 유지. jayverse-token과 jayverse-personas는 OpenZeppelin v5.1.0을 서브모듈 커밋으로 핀; Verex는 ctf-exchange를 통해 v4.7.0을 물려받음. 버전 올림은 읽은 릴리스 노트를 명시하는 PR.
  • JavaScript: 락파일을 강제한다. 오늘 CI 워크플로 하나가 --frozen-lockfile을 씀. 키나 돈을 건드리는 모든 앱, wallet, rabbit, verex api는 락파일 불일치에 빌드가 실패해야 함. 각 5분.
  • 돈을 움직이는 의존성을 센다. 레포마다 사용자 행동과 이전 사이 경로에 있는 패키지와 라이브러리를 나열. 보통 네댓 개. 매 올림마다 그것들만, 그것들은 반드시 읽기.
  • 두 진술, 두 주인. "코드가 안전"은 커밋에 대한 감사자의 진술. "공급망이 안전"은 핀, CI, 게시 권한이 누구에게 있는가에 대한 우리의 진술. 둘 모두 S&P 항목이 요구하는 권한 표에 적을 것.

확인된 것과 미확인

2026-09-18에 사고 기록(Parity 사후 분석 보도, TechCrunch 2017-11-07; npm과 Snyk의 event-stream 사후 분석)에서 확인한 것: 날짜, 초기화되지 않은 라이브러리와 selfdestruct 메커니즘, flatmap-stream 페이로드와 Copay 표적, 영향 받은 Copay 버전, 유지자 이관. 출처마다 약간 다르게 보도된 수치: 지갑 584~587개; 513,774 ETH; 주간 "약 2백만" 다운로드. 이 머신의 레포에서 확인한 Jayverse 상태: 서브모듈 버전, 임포트 개수, --frozen-lockfile이 있는 워크플로 하나. 미확인: Copay 백도어로 도난된 규모; 그 워크플로 외의 Jayverse 앱이 락파일을 강제하는지.

출처: TechCrunch — A major vulnerability has frozen hundreds of millions of dollars of Ethereum, 2017-11-07 · Parity 사후 분석 보도 — Coin Bureau · npm 블로그 — Details about the event-stream incident, 2018-11 · Snyk — A post-mortem of the malicious event-stream backdoor

핵심 표현

이 페이지의 영어 본문에서 배울 만한 단어와 표현, 뜻과 나온 자리.

Expression뜻 · 쓰이는 자리
bricked(기기·지갑이) 먹통이 되다, 못 쓰게 되다 · 소프트웨어 결함으로 자산에 접근 불가능해질 때. "about 587 wallets bricked"
hand over넘겨주다, 인계하다 · 패키지·프로젝트 관리권을 다른 사람에게 넘길 때. "had handed the package over"
drift from(기준·원본에서) 조금씩 벗어나다 · 배포된 코드가 감사받은 커밋과 달라질 때. "can drift from that commit without a decision"
decoration장식(에 불과한 것), 형식적인 것 · 실질적 효력이 없는 문서·절차를 비판할 때. "the audit is decoration"
pin (v.)(버전을) 고정하다 · 의존성 버전이 임의로 바뀌지 않게 못박을 때. "Pin the resolution"
diffusion of responsibility책임 분산(다들 남이 확인했겠지 하는 심리) · 인기 많은 패키지라서 아무도 안 읽고 넘어갈 때. "the diffusion of responsibility for defenders"
fail the build빌드를 실패시키다 · CI에서 조건 불일치 시 배포를 막을 때. "a mismatch fails the build"
a relationship in time시간에 걸친 관계 · 의존성을 한번 고정된 것이 아니라 계속 변하는 관계로 볼 때. "A dependency is a relationship in time"
BCH비트코인캐시(Bitcoin Cash) · 비트코인에서 하드포크된 알트코인 티커, 대량 보유 지갑이 탈취 표적이 된 자산 중 하나. "wallets holding more than 100 BTC or 1,000 BCH"
S&PS&P 글로벌(S&P Global) · 신용평가·금융데이터 대기업, OpenZeppelin을 인수해 감사 주체가 바뀌었음을 보여주는 사례로 언급됨. "the S&P acquisition of OpenZeppelin belongs next to these two cases"
Copay비트코인 지갑 앱(Bitcoin wallet application) · event-stream 악성코드가 실제로 표적으로 삼은 지갑 소프트웨어, 대형 지갑의 시드구문이 탈취됨. "activated only inside Copay, a Bitcoin wallet app"

← 전체 기술 노트 · 워크스페이스 인덱스 · 맨 위 ↑