Ringkasan Utama
Worm npm keyv pada 4 Agustus 2026 dirilis lewat GitHub Actions dan OIDC publishing npm yang sah dengan provenance SLSA valid, karena provenance hanya membuktikan integritas build, bukan integritas source. Pasang gate rilis berbasis source yang direview, kebijakan install script, dan korelasi rilis ke commit, lalu hapus persistence sebelum merotasi kredensial.
Reaksi pertama banyak orang pada insiden keyv adalah mengecek badge-nya. keyv 6.0.0 yang beracun punya attestation provenance, menyebut GitHub Actions sebagai publisher, dan semua signature lolos verifikasi. Di situlah bagian yang tidak nyaman: kontrolnya bekerja persis seperti yang dirancang, tetapi package-nya tetap membawa worm pencuri kredensial.
Tulisan ini membaca insiden tersebut lewat write-up primer yang bisa saya akses: analisis Harness tentang worm ChainDrop, timeline commit demi commit dari lilting.ch, serta dokumentasi npm dan SLSA tentang apa yang sebenarnya diklaim provenance. Fokusnya satu pertanyaan. Mengapa provenance valid tidak menolong, dan apa yang perlu dicek release gate selain signature? Tidak ada exploit di sini; ini bacaan dari sisi defender, lengkap dengan urutan pembersihan yang paling penting.
Apa yang terjadi pada 4 Agustus 2026, berurutan
Harness menggambarkan worm npm yang menyebar sendiri pada 4 Agustus dari ekosistem keyv dan cacheable ke package milik beberapa organisasi yang tidak berhubungan. Write-up lilting.ch merekonstruksi sisi repository keyv lengkap dengan waktu, semuanya UTC:
- 09:02:37 - commit ee2681a9 menambahkan file payload dan sebuah lifecycle hook.
- 09:04:30 - commit d8c850c7 menambahkan .claude/settings.json dan .vscode/tasks.json. Artikel itu mencatat commit ini punya signature terverifikasi, tetapi field author bebas diisi, jadi signature valid tidak menunjukkan siapa pemegang token.
- 09:23:50 - commit f97eabcd menghapus baris tes preinstall.
- 09:35:00 - keyv 6.0.0 dipublikasikan dengan preinstall berbahaya yang menjalankan node setup.mjs.
Harness, mengutip SafeDep, menghitung 2.234 versi beracun di 444 nama package yang menjangkau 12 organisasi dalam kurang dari empat jam. Empat package inti, keyv, flat-cache, file-entry-cache, dan cacheable-request, menerima sekitar 1,88 miliar download per bulan secara gabungan. Angka berbeda antar vendor: lilting.ch mengutip Aikido di 434 package dengan 1.381 versi, dan Harness sendiri menyatakan angkanya mencerminkan laporan per 5 Agustus dan bisa berubah. Saya memakai angka SafeDep karena dua sumber sepakat, dan semua total saya anggap snapshot.
Mengapa provenance-nya valid: ia menjawab pertanyaan yang sempit
Dokumentasi npm menyebut provenance menautkan package ke source code dan instruksi build-nya, dan menegaskan bahwa ia tidak menjamin package bebas kode berbahaya. SLSA lebih sempit lagi. Build track v1.0 ada agar consumer bisa memverifikasi artifact dibangun sesuai harapan, dan spesifikasinya mencatat aspek source dihapus demi fokus ke track itu. Pada kasus keyv, build-nya memang persis hasil workflow. Inputnya yang sudah beracun, karena penyerang mendorong payload ke repository sebelum tag dibangun. Harness mengutip Socket: provenance membuktikan integritas build, bukan integritas source.
| Pertanyaan yang peduli bagi consumer | Apakah provenance valid menjawabnya? | Di mana jawabannya sebenarnya |
|---|---|---|
| Workflow dan repository mana yang membangun tarball ini? | Ya | Attestation itu sendiri, ditandatangani dan dicatat Sigstore menurut dokumentasi npm |
| Apakah source direview sebelum rilis? | Tidak | Branch protection, riwayat pull request yang di-merge, required reviewers |
| Apakah rilis cocok dengan commit atau tag yang disetujui? | Hanya cocok dengan commit yang ia sebut sendiri | Gate yang mengkorelasikan tag, pull request, dan rilis sebelumnya |
| Apakah ada lifecycle script atau file workflow yang berubah? | Tidak | Diff package.json, workflow CI, konfigurasi editor dan agent |
| Haruskah install script berjalan di mesin saya? | Tidak | Kebijakan install Anda sendiri, misalnya ignore-scripts |
| Secret apa yang bisa dijangkau runner? | Tidak | Isolasi runner dan identitas least-privilege |
Bacalah tabel dari kolom kedua. Provenance adalah jawaban kuat untuk pertanyaan yang jarang diajukan penyerang, yaitu apakah ada yang menukar tarball setelah build. Serangan ini tidak menukar apa pun. Ia bergeser ke hulu, sebelum build, satu-satunya tempat yang tidak dilihat attestation.
OIDC trusted publishing menghapus token, bukan kepercayaan
Trusted publishing npm memungkinkan workflow mem-publish lewat OpenID Connect dengan kredensial berumur pendek tanpa npm token jangka panjang. Per package Anda mengatur user atau organisasi GitHub, repository, nama file workflow, dan nama environment opsional. Ini kemajuan nyata: tidak ada token di secret store yang bisa dicuri. Namun identitasnya kini adalah workflow, sehingga siapa pun yang bisa memasukkan commit dan tag ke repository workflow itu bisa mem-publish.
# .github/workflows/publish.yml
# This file is what npm's trusted-publisher entry points at (workflow filename,
# no path). Anything that lands on a tag this workflow builds gets published,
# so the question is who can put commits and tags there, not who holds a token.
name: publish
on:
push:
tags: ["v*"]
permissions:
contents: read
id-token: write # mints the OIDC token npm exchanges for a publish
jobs:
publish:
runs-on: ubuntu-latest
# Optional environment name on the npm side. Protection rules on the GitHub
# side are where a human pause can be forced before the publish step.
environment: npm-release
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
registry-url: https://registry.npmjs.org
# Wrong: a plain "npm ci" runs every dependency's lifecycle scripts
# on the runner that is about to hold the publish identity.
# Right: install without scripts, build explicitly.
- run: npm ci --ignore-scripts
- run: npm run build
- run: npm publishAda tiga detail dari halaman npm yang perlu diingat. Trusted publishing tidak menonaktifkan token yang sudah ada, dan npm menyarankan mewajibkan two-factor authentication serta melarang token, lalu mencabut token yang tidak terpakai. Self-hosted runner tidak didukung. Dan nama environment opsional itu adalah tempat approval manusia bisa diletakkan, satu-satunya langkah di alur ini yang tidak otomatis.
Workflow di atas adalah bentuk yang bisa dieksploitasi sekaligus bentuk yang perlu diperkeras. Perhatikan langkah install. Runner yang memegang identitas publish tidak boleh menjalankan lifecycle script dependency, karena itu primitif eksekusi kode yang sama yang dipakai worm di mesin yang ia jangkau.
Persistence yang tidak butuh npm install
Hal yang membedakan insiden ini dari worm npm lama adalah tempat ia bersembunyi sesudahnya. Harness melaporkan persistence yang terkait konfigurasi proyek VS Code dan Claude Code, sehingga membuka repository terdampak di salah satunya bisa memicu kode tanpa install package apa pun. Artikel lilting.ch menyebut file-nya: hook SessionStart di .claude/settings.json yang menjalankan node .vscode/setup.mjs saat sesi Claude Code dimulai, dan task folderOpen bernama Environment Setup di .vscode/tasks.json. Ditambahkan bahwa jalur VS Code mensyaratkan workspace dipercaya dan pengguna memilih Allow.
Mekanisme kedua adalah token monitor. Menurut lilting.ch, script di ~/.local/bin/gh-token-monitor.sh memanggil api.github.com/user tiap 60 detik dengan token GitHub curian. Jika statusnya 4xx, yaitu yang dikembalikan token yang dicabut, ia menjalankan handler dari jarak jauh lewat eval. Harness memberi peringatan serupa secara umum: temukan dan hapus monitor sebelum mencabut atau merotasi kredensial. Script di bawah hanya menampilkan daftar; ia tidak menghapus apa pun.
#!/usr/bin/env bash
# Hunt for the persistence described in the lilting.ch write-up. Read-only:
# it lists, it deletes nothing. Run it BEFORE you revoke any token.
# 1. The token monitor (a 60-second poll of api.github.com/user).
ls -la ~/.local/bin/gh-token-monitor.sh \
~/.config/gh-token-monitor/ \
~/Library/LaunchAgents/com.user.gh-token-monitor.plist \
~/.config/systemd/user/gh-token-monitor.service \
/tmp/gh-token-monitor.*.log 2>/dev/null
# 2. Editor and agent hooks planted inside repositories you cloned.
# These fire on folder open or session start, with no npm install at all.
find ~/code -type d \( -name node_modules -o -name .git \) -prune -o \
\( -path '*/.claude/settings.json' -o -path '*/.claude/setup.mjs' \
-o -path '*/.claude/math_init.js' -o -path '*/.vscode/tasks.json' \
-o -path '*/.vscode/setup.mjs' \) -print
# 3. Which of your repos resolved a poisoned keyv at all?
grep -rn --include=package-lock.json -E '"node_modules/keyv"' -A2 ~/code \
| grep -E '"version": "6\.0\.0"'Jalankan dari mesin yang masih bisa dipercaya, dan anggap setiap temuan sebagai alasan memutus jaringan mesin itu sebelum menyentuh token. File di dalam repository hasil clone juga dihitung. Jika Anda meng-clone repository keyv selama jendela waktu beracun, jalur IDE tetap ada walaupun Anda tidak pernah menjalankan npm install.
Kebijakan install: tiga setting npm yang akan berarti
Rilis berbahaya memakai hook preinstall, jadi kontrol paling langsung adalah menolak menjalankan lifecycle script dari dependency. Harness menyebut menonaktifkan lifecycle script di CI jika tidak diperlukan sebagai kontrol umum, dan npm mendokumentasikan setting di bawah. min-release-age dan allow-git dijelaskan di halaman dokumentasi CLI 11.21; pastikan versi npm Anda mendukungnya sebelum mengandalkannya.
# .npmrc (npm 11.x config, per docs.npmjs.com/cli/v11/using-npm/config)
# Dependencies do not get to run code at install time. Default is false.
ignore-scripts=true
# Only resolve versions published more than N days ago. Default is null (off).
# The poisoned keyv releases were live for hours, not days, so a cooldown
# starves this class of attack. It does not stop a long-lived compromise.
min-release-age=7
# Do not let a dependency pull from a git reference you never reviewed.
# "root" allows only git dependencies listed in your own package.json.
allow-git=rootCooldown adalah taruhan pada berapa lama penyerang tidak terdeteksi. Rilis keyv 6.0.0 keluar pukul 09:35 UTC pada 4 Agustus dan gelombangnya selesai dalam hitungan jam menurut Harness, jadi jeda tujuh hari akan melewatinya. Ia tidak menolong untuk package yang tetap beracun sebulan. ignore-scripts adalah kontrol yang tidak bergantung pada waktu, dengan biaya berupa native module yang butuh langkah build eksplisit.
Release gate yang melihat source, bukan signature
Harness berargumen bahwa artifact bertanda tangan hanyalah satu input keputusan risiko, dan menyebut apa yang perlu dievaluasi: review source, script baru, perilaku tidak biasa, dan konsistensi source ke artifact. Berikut gate yang saya jalankan di job yang harus lulus sebelum job publish. Ia mengajukan tiga pertanyaan murah yang gagal pada push langsung ke default branch. Ini sketsa saya sendiri dari ide-ide tersebut, bukan sesuatu yang diterbitkan kedua write-up, dan belum saya jalankan terhadap riwayat keyv.
#!/usr/bin/env bash
# release-gate.sh: run in a job BEFORE the publish job. Fails the release
# when the tagged commit does not look like reviewed, expected work.
set -euo pipefail
TAG="$1"; PREV="$2"; REPO="$3" # e.g. v6.0.0 v5.9.0 acme/widget
SHA=$(git rev-list -n1 "$TAG")
# 1. Was the tagged commit reached through a merged pull request?
# A payload pushed straight to the default branch has none.
MERGED=$(gh api "repos/$REPO/commits/$SHA/pulls" \
--jq '[.[] | select(.merged_at != null)] | length')
[ "$MERGED" -ge 1 ] || { echo "no merged PR for $SHA"; exit 1; }
# 2. Did this release touch things a library release never should?
git diff --name-only "$PREV" "$TAG" -- .claude .vscode .github/workflows \
| grep . && { echo "agent, editor or CI config changed"; exit 1; } || true
# 3. Did a lifecycle script appear in package.json?
git diff "$PREV" "$TAG" -- package.json \
| grep -E '^\+.*"(pre|post)?install"' \
&& { echo "install script added"; exit 1; } || true
echo "gate passed for $TAG"Cek pertama paling kuat. Timeline lilting.ch menunjukkan commit mendarat berselang menit dan tag dibangun tepat sesudahnya. Aturan bahwa commit bertag harus terjangkau lewat pull request yang di-merge akan menolaknya, jika repository menerapkannya. Cek kedua dan ketiga berisik pada proyek yang memang sah mengedit workflow, jadi perlakukan sebagai pemicu review manusia, bukan larangan otomatis.
Pasang gate pada job publish dengan needs, dan pasang required reviewers pada GitHub environment yang dipakai job itu. Gate yang hanya melapor adalah dashboard; gate dalam rantai dependency langkah publish adalah kontrol.
Di mana kontrol-kontrol ini kurang
Semua ini tidak gratis. ignore-scripts merusak package yang meng-compile kode native atau mengunduh binary di install script, jadi Anda membayar dengan allow list dan sedikit rekayasa build. Release gate pada library kecil dengan satu maintainer tidak punya orang untuk mereview pull request, sehingga cek pertama jadi stempel belaka. Dan cooldown tujuh hari menunda perbaikan keamanan asli bersama yang beracun.
Desain worm itu sendiri juga membatasi apa yang bisa Anda verifikasi. Unpublish menghapus versi beracun dari registry, tetapi lilting.ch mencatat lockfile, cache CI, dan direktori node_modules yang sudah di-unpack masih menyimpannya. Angka vendor juga berbeda, jadi cek versi yang ter-resolve di lockfile Anda sendiri alih-alih memercayai satu daftar nama package buruk.
Kesalahan yang mahal adalah merotasi lebih dulu. Jika sebuah mesin pernah menjalankan versi terdampak, token monitor mungkin sedang polling dengan token yang akan dicabut. Hapus monitor dan hook-nya, putus jaringan mesin, baru rotasi dari lingkungan yang bersih.
Saya juga tidak membaca ini sebagai argumen melawan provenance. Ia tetap menggagalkan serangan lain, yaitu pertukaran tarball, dan SLSA Build L3 ditujukan untuk tampering saat build. Pelajarannya soal cakupan: kontrol yang membuktikan di mana sesuatu dibangun tidak mengatakan apa-apa tentang apakah sesuatu itu layak dibangun.
Yang dikerjakan hari Senin
Diurutkan dari yang paling murah dan paling banyak menghapus risiko. Urutan pembersihan mengikuti lilting.ch dan langkah insiden di artikel Harness.
- Cari di lockfile dan cache CI Anda keyv 6.0.0 yang ter-resolve dan salah satu dari empat package inti yang dirilis 4 Agustus, lalu catat mesin developer dan runner mana yang meng-install-nya.
- Di mesin yang pernah, putus jaringan, jalankan script daftar di atas, lalu hapus file token monitor serta file .claude dan .vscode yang ditanam.
- Periksa .github/workflows untuk job bernama Run Copilot atau Copilot Setup yang tidak Anda tambahkan, dan tinjau riwayat CI jika menemukannya.
- Dari mesin bersih, rotasi kredensial npm, GitHub, cloud, Kubernetes, Vault, SSH, Docker, dan database, lalu bangun ulang sistem terdampak dan artifact hilirnya.
- Aktifkan ignore-scripts di CI dan di workflow publish, dan uji min-release-age di satu repository dulu.
- Tambahkan release gate ke setiap package yang Anda publish, pasang required reviewers pada environment publish, dan cabut npm token yang sudah tidak perlu berkat trusted publishing.
Jika Anda memelihara package, ingat bahwa workflow publish Anda kini adalah kredensialnya. Lindungi aturan branch dan tag yang memberinya input seketat Anda dulu melindungi npm token.
Provenance valid mengatakan hal yang benar: GitHub Actions membangun kode ini. Ia tidak bisa mengatakan apakah kode itu seharusnya ada di sana. Perlakukan signature sebagai satu input, letakkan source yang direview, kebijakan install, dan korelasi rilis di sampingnya, dan saat worm mendarat, hapus yang mengawasi token Anda sebelum Anda mencabutnya.
Sumber
- Harness: worm npm ChainDrop menunjukkan mengapa provenance valid tidak cukup
- lilting.ch: keyv 6.0.0 membawa provenance npm valid dan daemon yang aktif saat Anda mencabut token
- The Hacker News: worm npm terkait keyv meracuni ratusan package
- npm Docs: generating provenance statements
- npm Docs: trusted publishing untuk package npm
- SLSA: build track levels, v1.0
- npm Docs: konfigurasi CLI (ignore-scripts, min-release-age, allow-git)

