
keyv/cacheable 공급망 공격용 스캐너: 손상된 npm 패키지를 탐지하고, 페이로드 해시를 검증하며, repo 및 host 모드에서 지속성 임플란트를 찾아냅니다.
English | Português
**keyv/cacheable 공급망 사고("Shai-Hulud: Here We Go Again", 2026년 8월 4일)**를 위한 독립형 스캐너 — 클라우드/CI 자격 증명을 탈취하고 데드맨 스위치와 함께 지속성을 심는 자가 증식 웜에 의해 손상된 440개 이상의 npm 패키지.
몇 분 안에, 아무것도 설치하지 않고 탐지합니다:
package-lock.json, npm-shrinkwrap.json, yarn.lock(v1 및 Berry), pnpm-lock.yaml, bun.lock의 손상된 패키지 — 전이적 의존성을 포함하며 전체 체인(예: eslint → file-entry-cache → flat-cache → [email protected])을 표시합니다;node_modules에 설치된 페이로드(알려진 아티팩트의 이름 + SHA-256 해시);SUSPECT로 표시되며, 해시 없이는 확정하지 않습니다;.claude/settings.json 및 .vscode/tasks.json의 훅, 임시 아티팩트(bun-dl-*);keyv/cacheable 계열의 관리자 계정이 손상되었습니다. 공격자는 "preinstall": "node setup.mjs" 훅이 포함된 새 버전을 게시했습니다 — 패키지가 설치되기 전에, npm install을 실행한 사람의 권한으로 실행되는 코드입니다.setup.mjs는 GitHub에서 Bun 런타임을 다운로드하여 그 안에서 페이로드를 실행합니다 — node 프로세스만 모니터링하는 도구에 대한 회피 수법입니다.Math_Symbol.js(약 728KB, 난독화됨)는 자격 증명을 탈취합니다: AWS 인스턴스 메타데이터, AWS/GCP/Azure 키, Vault 토큰, Kubernetes 서비스 계정, GitHub Actions 시크릿, npm 토큰, 그리고 디스크의 개인 키와 Bearer 토큰을 찾는 일반적인 정규식 스캔까지 포함합니다.npm install/npm ci를 실행한 모든 사람. --ignore-scripts를 사용했다면 훅이 실행되지 않았습니다..claude/settings.json(SessionStart)과 .vscode/tasks.json(folderOpen)에 자동 시작 훅이 추가되었으며, 이 훅은 클론한 폴더가 열릴 때 로더를 실행합니다 — npm install도, 설치되는 것도 없습니다. 여기에는 사고를 조사하기 위해 저장소를 클론한 사람과 디렉터리를 연 AI 코딩 에이전트가 포함됩니다 — AI 에이전트 훅(.claude/)이 공급망 벡터로 사용된 최초의 공개 사례 중 하나입니다.임플란트는 LaunchAgent(macOS) 또는 systemd 사용자 서비스 + loginctl enable-linger(Linux)로 유지되는 "워처"(gh-token-monitor)를 설치합니다. 60초마다 API를 통해 탈취한 GitHub 토큰의 유효성을 검증합니다. 토큰이 유효한 동안에는 아무 일도 일어나지 않습니다. 응답이 4xx로 바뀌면 — 즉 토큰을 폐기하는 순간 — eval을 통해 ~/.config/gh-token-monitor/handler의 내용, 즉 공격자가 원격으로 정의한 임의의 명령을 실행합니다. 공개 분석에서는 그 내용이 무엇인지 알 수 없습니다 — 데이터 파괴, 재감염, 랜섬웨어일 수도 있고 아무것도 아닐 수도 있습니다. 위험을 평가할 수 없기 때문에 대응 순서가 절대적입니다.
대응을 바꾸는 세 가지 속성:
host 모드에서 이에 대해 경고합니다.[email protected]은 통과된 SLSA 증명과 함께 배포되었습니다. 출처 증명(provenance)은 소스가 아니라 빌드 무결성을 증명합니다. 합법적인 워크플로가 이미 트로이 목마가 심어진 코드를 컴파일한 것입니다.package.json(preinstall 훅)과 패키지에 추가된 두 개의 새 파일(setup.mjs, Math_Symbol.js)에 있습니다.eslint → file-entry-cache → flat-cache → keyv입니다. 그래서 스캐너는 모든 탐지 결과에서 체인을 표시합니다.scan.mjs는 다음과 같은 속성을 가집니다 — 공급망 사고에 대응하는 모든 사람에게 중요합니다:
npm install이 필요 없습니다. 실행 전 15분 안에 scan.mjs 전체를 감사할 수 있습니다.--update(최신 IOC 매니페스트 다운로드)뿐입니다.docker run --network=none 또는 격리된 머신: scan.mjs + iocs.json만 복사하면 됩니다.요구 사항: Node.js ≥ 18(npm이 있는 모든 머신에는 이미 있습니다). scan.mjs + iocs.json 두 파일을 다운로드하면 끝입니다 — 설치가 필요 없습니다.
알림: 이 전체 저장소를 클론했다면,
fixtures/폴더에는 테스트에 사용된 비활성(inert) IOC가 들어 있습니다(실제 이름과 버전, 더미 콘텐츠 — 악성 코드 없음). 스캐너는 이를 자동으로 건너뛰고 출력에서 경고합니다. 이 폴더의 탐지 결과는 의도적으로 스캔할 때만 나타납니다.
서로 다른 질문에 답하는 두 가지 실행 모드가 있으며, 그것이 어디서 실행할지 결정합니다:
repo 모드는 잠금 파일과 node_modules를 읽습니다 — 잠금 파일은 git에 있으므로 중앙 집중화할 수 있습니다: 한 사람이 회사의 모든 저장소를 스캔할 수 있습니다.host 모드는 코드가 실행된 머신에 존재하는 임플란트(워처, LaunchAgent/systemd, IDE 훅)를 찾습니다 — 이는 git에 없으므로 중앙 집중화할 수 없습니다.node scan.mjs repo /folder/with/all/the/repos --json=result.json --html=report.html
"어떤 프로젝트가 노출되었는지"에 몇 분 안에, 아무도 관여시키지 않고 답합니다. 여러 경로를 허용하며 하위 디렉터리를 탐색합니다(모노레포와 워크스페이스 포함).
탐지 결과가 있는 각 프로젝트에 대해 2026-08-04 09:35 UTC 이후로 해당 프로젝트를 만진 사람을 식별합니다(git log, CI 로그). 그 사람들이 자신의 머신에서 실행합니다:
node scan.mjs # current directory + host, in ~30 seconds
범위: (a) 해당 기간에 npm install/npm ci를 실행한 사람; 또는 (b) 단지 클론하고 VS Code나 AI 에이전트에서 폴더를 열기만 한 사람 — 벡터 B는 설치가 필요 없습니다.
비용이 약 30초이고 대상 선별 과정에 누수가 있을 수 있으므로(우연한 클론, 개인 프로젝트), 가장 안전한 사내 지침은 다음과 같습니다: 모든 개발자가 node scan.mjs를 한 번 실행하고 --json/--html 결과를 AppSec에 보냅니다. 전송은 설계상 수동입니다 — 스캐너에는 텔레메트리가 없습니다(외부 전송 제로).
최우선 순위: 가장 가치 있는 자격 증명이 있는 곳입니다. 여기서 스캐너는 두 가지 다른 역할을 합니다 — 하나는 과거용, 하나는 미래용입니다:
과거 분류 — 판단을 위해 러너를 스캔하지 마십시오. "이 러너가 감염되었는가?"라는 질문은 스캔으로 답할 수 없습니다. 어떤 작업이 08-04 이후 --ignore-scripts 없이 영향받은 버전을 설치했다면, 그 순간 이미 자격 증명이 탈취된 것이며 러너 호스트는 증거를 거의 남기지 않습니다(임시 러너는 작업이 끝나면 컨테이너를 파기합니다; 워처는 약 24시간 안에 스스로 지워집니다). 이 질문에 답하는 것은 1단계의 잠금 파일과 CI 로그입니다. 답이 "예, 설치했습니다"라면: 러너를 재구축하고 시크릿을 교체하십시오 — 러너는 임시적이므로 정리할 이유가 없습니다.
향후 예방 — 그렇습니다, 파이프라인에서 실행하십시오. 스캐너를 빌드 단계로 추가하되, repo 모드에서 체크아웃 후, npm install 전에 실행합니다. 러너 호스트를 검사하는 것이 아니라 설치될 코드를 검사하며, 악성 preinstall이 실행될 기회를 얻기 전에 종료 코드로 빌드를 실패시킵니다:
# example (GitHub Actions / GitLab CI — adapt):
- run: node scan.mjs repo . --json # exit 0 clean · 1 findings · 2 COMPROMISED
- run: npm ci --ignore-scripts # only runs if the previous step passed
node scan.mjs # scan the current directory + the host
node scan.mjs repo /path/a /path/b
node scan.mjs host # persistence/implants on the machine only
node scan.mjs repo . --json=result.json --html=report.html
node scan.mjs --update # update iocs.json (the only network operation)
| 수준 | 의미 | 조치 |
|---|---|---|
COMPROMISED | node_modules에 악성 버전이 설치됨, 해시로 확인된 페이로드, 또는 지속성 임플란트 발견 | 호스트를 손상된 것으로 간주하고 대응 순서를 따르세요 — 자격 증명을 교체하기 전에 임플란트를 제거하세요 |
EXPOSED | 잠금 파일에 고정된 악성 버전, 실행 증거 없음 | 안전한 버전을 고정하고, node_modules를 삭제한 후 --ignore-scripts로 재설치하세요 |
AT_RISK | 악성 버전을 허용하는 package.json의 범위(^/~) | 정확한 버전을 고정하거나 레지스트리 프록시에서 차단하세요 |
SUSPECT | 휴리스틱(해시 불일치가 있는 웜 파일명, 의심스러운 수명주기 스크립트) | 수동으로 검사하세요 — 새 변종이거나 오탐일 수 있습니다 |
INFO | 벡터는 존재하지만 IOC가 없음(예: 일반적인 folderOpen 작업) | 검토하세요 |
--html은 타임스탬프, 호스트 이름, IOC 매니페스트 버전 및 스캐너 자체의 SHA-256이 포함된 단일 파일 보고서를 생성합니다 — 사고 통지에 첨부하고 감사 추적으로 사용할 수 있습니다.
아직 어떤 자격 증명도 폐기하거나 교체하지 마십시오 — 그것이 함정의 방아쇠입니다. 순서:
1. 격리(ISOLATE) — 머신을 네트워크에서 차단하세요. 안전합니다: HTTP 응답이 없으면 4xx도 없고, 함정이 발동하지 않으며, 유출도 중단됩니다. 전원을 끄지 마십시오(휘발성 메모리는 증거입니다).
2. 보존(PRESERVE) — 무엇이든 삭제하기 전에(워처는 약 24시간 후 자동 파괴됩니다):
mkdir -p /tmp/evidence && cp -r ~/.config/gh-token-monitor /tmp/evidence/ 2>/dev/null
cp /tmp/gh-token-monitor.*.log /tmp/evidence/ 2>/dev/null
shasum -a 256 /tmp/evidence/* 2>/dev/null
handler 파일은 실행될 공격자의 명령입니다 — 실행하지 말고, 셸에 붙여넣지 마십시오; 비활성 텍스트로 취급하세요. started_at 파일은 노출 기간의 경계를 정의합니다(감사자와 규제 기관이 이를 요청할 것입니다).
3. 제거(ERADICATE) — 먼저 워처 프로세스를 종료한 다음:
# macOS
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.user.gh-token-monitor.plist
rm -f ~/Library/LaunchAgents/com.user.gh-token-monitor.plist