Ma reggel egy Slack üzenetre ébredtem a security leadtől: “az axiost megtörték npm-en.” Azt hittem vicc. Az axiosnak heti 60 millió letöltése van. Az ember feltételezi, hogy biztonságos, mert mindenki használja.
Nem volt vicc.
Mi történt pontosan
Két kártékony verzió jelent meg npm-en éjszaka: [email protected] és [email protected]. A támadó megszerezte az egyik fő maintainer npm hitelesítő adatait, átírta a fiók emailjét egy ProtonMail címre, és kézzel publisholt npm CLI-vel. Nem volt pull request. Nem volt CI futás. Nem volt code review. Csak egy npm publish egy lopott fiókból.
Az okos rész: a kártékony kód nem magában az axiosban van. Mindkét verzió behúz egy [email protected] nevű függőséget, ami postinstall scriptként fut. Ez a script egy cross-platform RAT dropper. Hazatelefonál egy C2 szerverre, letölti a második lépcsős payloadot az adott OS-re, lefuttatja, aztán törli magát és felülírja a saját package.json-ját egy tiszta verzióval. Ha utólag nézed meg a node_modules-t, minden normálisnak tűnik.
A támadó 18 órával előre felkészítette a hamis csomagot, először egy tiszta v4.2.0-t publisholt, hogy ne triggerelődjön az “új csomag” riasztás. Mindkét axios branch-et 39 percen belül megtámadta. Ez tervezett volt.
Első lépés: megnéztem, érint-e minket
Mielőtt pánikolnék, tudnom kellett, hogy bármelyik projektünk behúzta-e a rossz verziót:
# Lockfile-ok ellenőrzése a kompromittált verziókra
grep -r "[email protected]\|[email protected]\|plain-crypto-js" */package-lock.json */yarn.lock */pnpm-lock.yaml 2>/dev/null
Tiszták voltunk. A lockfile-jaink az axios 1.7.9-re mutattak a legtöbb projektben és 1.14.0-ra egy újabb service-ben. De a “tiszta a lockfile-ban” nem jelenti, hogy mindenhol tiszta. A CI artifact-okat is megnéztem:
# Ellenőrizni, hogy bármelyik friss CI futás telepítette-e a rossz verziót
for f in /tmp/ci-artifacts/*/npm-ls.txt; do
grep -l "[email protected]\|[email protected]\|plain-crypto-js" "$f"
done
Semmi. De ha nálad megvan, a StepSecurity advisory egyértelmű: tekintsd kompromittáltnak a gépet. Rotáld az összes credential-t, ami elérhető volt abból a környezetből.
Miért mentettek meg a lockfile-ok (ezúttal)
Pontos verziókat pinelünk és commitoljuk a lockfile-okat. Mindig. Ez az egyetlen gyakorlat, ami ma megmentett minket. Ha npm install-t futtatsz lockfile nélkül, vagy a lockfile-od nincs commitolva, minden build-del szerencsejátékot játszol.
A lényeg viszont az, hogy a lockfile-ok csak akkor védenek, ha a CI tényleg használja őket:
# ROSSZ - ez frissítheti a lockfile-t és behúzhat új verziókat
npm install
# JÓ - ez pontosan betartja a lockfile-t
npm ci
Végignéztem a CI konfigjainkat és találtam két repót, ami még npm install-t használt a Dockerfile-ban. Azonnal javítottam:
# Előtte (veszélyes)
COPY package*.json ./
RUN npm install --production
# Utána (lezárt)
COPY package*.json package-lock.json ./
RUN npm ci --production
A különbség számít. Az npm ci elhasal, ha a lockfile nincs szinkronban a package.json-nal, ahelyett, hogy csendben frissítené. Ez a hiba egy feature.
Postinstall scriptek lezárása
Az axios támadás teljes egészében postinstall scriptekre épül. A kártékony plain-crypto-js csomag node setup.js-t futtat telepítéskor, és ott fut le a payload. Globálisan le tudod tiltani:
# A .npmrc-ben (projekt vagy CI szinten)
ignore-scripts=true
A gond az, hogy néhány legitim csomagnak szüksége van postinstall scriptekre. Az esbuild, sharp, node-sass mind letöltenek platform-specifikus binárisokat telepítéskor. Szóval a teljes tiltás dolgokat tör el.
Ehelyett egy allowlist megközelítést alkalmaztam:
# .npmrc
ignore-scripts=true
# Aztán explicit futtatjuk a scripteket azoknál a csomagoknál, amelyeknek kell
# A CI pipeline-ban:
npm ci --ignore-scripts
npx --yes node-gyp rebuild # csak ha kellenek natív modulok
npm audit beépítése a pipeline-ba
Már futtattuk az npm audit-ot, de csak heti ütemezéssel. Ma után minden build-nél fut:
# .github/workflows/ci.yml
- name: Security audit
run: |
npm audit --audit-level=high
npx is-my-node-vulnerable
Hozzáadtam egy lépést is, ami ellenőrzi, hogy bármelyik függőség az elmúlt 24 órában lett-e publisholva. A friss publishok közvetlenül a build előtt gyanúsak:
#!/bin/bash
# check-fresh-deps.sh
set -euo pipefail
npm ls --all --json 2>/dev/null | jq -r '
[.. | .resolved? // empty] | unique[]
' | while read -r url; do
pkg=$(echo "$url" | grep -oP '(?<=/)(@[^/]+/[^/]+|[^/]+)(?=/-/)')
version=$(echo "$url" | grep -oP '(?<=-)\d+\.\d+\.\d+(?=\.tgz)')
if [ -n "$pkg" ] && [ -n "$version" ]; then
pub_time=$(npm view "$pkg@$version" time --json 2>/dev/null | jq -r ".\"$version\"" 2>/dev/null)
if [ -n "$pub_time" ] && [ "$pub_time" != "null" ]; then
pub_epoch=$(date -d "$pub_time" +%s 2>/dev/null || echo 0)
now_epoch=$(date +%s)
age_hours=$(( (now_epoch - pub_epoch) / 3600 ))
if [ "$age_hours" -lt 24 ]; then
echo "FIGYELMEZTETÉS: $pkg@$version $age_hours órája lett publisholva"
fi
fi
fi
done
Ez nem tökéletes, de azonnal jelzett volna a [email protected]-nél.
Hálózati egress kontroll a CI-ban
A StepSecurity Harden-Runner-je úgy kapta el ezt a támadást, hogy figyeli a kimenő hálózati kapcsolatokat a CI runnerekből. A RAT megpróbált hazatelefonálni az sfrclak.com:8000-re, amit azonnal anomáliaként jelölt.
Ha GitHub Actions-t használsz, a Harden-Runner hozzáadása egy sor:
- uses: step-security/harden-runner@v2
with:
egress-policy: audit # kezdd audit-tal, utána állítsd block-ra
allowed-endpoints: >
registry.npmjs.org:443
github.com:443
api.github.com:443
Self-hosted runnerekhez vagy más CI rendszerekhez hasonlót érhetsz el iptables-szel:
# A CI runneren korlátozd a kimenő forgalmat ismert registry-kre
iptables -A OUTPUT -p tcp -d registry.npmjs.org --dport 443 -j ACCEPT
iptables -A OUTPUT -p tcp -d github.com --dport 443 -j ACCEPT
iptables -A OUTPUT -p tcp -d api.github.com --dport 443 -j ACCEPT
iptables -A OUTPUT -p tcp --dport 443 -j LOG --log-prefix "BLOCKED: "
iptables -A OUTPUT -p tcp --dport 443 -j DROP
Ez hónapok óta rajta volt a todo listámon. Ma volt a lökés, ami kellett. Mostanra minden CI runnerünkön van egress policy.
Privát registry mint cache
A lockfile-okon túl érdemes privát registry-t használni, ami proxy-zza az npm-et és cache-eli a csomagokat:
# Verdaccio config npm proxyzáshoz cachinggel
uplinks:
npmjs:
url: https://registry.npmjs.org/
cache: true
packages:
'**':
access: $all
proxy: npmjs
Ez egy helyi cache-t ad, ami nem frissül automatikusan. Még ha egy kártékony verzió megjelenik is npm-en, a build-jeid a cache-elt tiszta verzióból húznak, amíg explicit nem frissítesz.
Mit változtattam ma
A teljes lista arról, mit csináltam a repóinkban kb. 3 óra alatt:
- Végigauditáltam minden lockfile-t a kompromittált axios verziókra
- Minden
npm install-t lecseréltemnpm ci-re a CI-ban - Hozzáadtam az
ignore-scripts=true-t az.npmrc-hez minden projektben, explicit script futtatással a csomagoknak, amiknek kell - Beépítettem az
npm audit --audit-level=high-t blokkoló CI lépésként - Telepítettem a friss-dependency ellenőrző scriptet
- Beállítottam egress policy-kat a GitHub Actions runnereinken
- Dokumentáltam az incidenst a security runbookunkban
Ebből semmi nem bonyolult. A legtöbbjét már régen meg kellett volna csinálni. Az npm egyik legnépszerűbb csomagja elleni támadás kellett hozzá, hogy tényleg megcsináljam.
A nagyobb probléma
Az npm ökoszisztémának alapvető bizalmi problémája van. Bárki, akinek van maintainer hozzáférése, bármit publisholhat, és a világ CI pipeline-jai kérdés nélkül lefuttatják az adott csomag kódját. Nincs kötelező code review. Nincs kötelező 2FA publisholáshoz (bár az npm nyomja). Nincs késleltetés a publish és az elérhetőség között.
Az axios támadó pontosan tudta, hogyan kell ezt kihasználni. Előkészít egy tiszta csomagot. Vár. Publisholja a mérget. Hagyja, hogy a világ CI pipeline-jai futtassák a kódját. Törli a nyomokat.
Amíg az ökoszisztéma nem oldja meg ezt registry szinten, a védekezés rajtunk múlik. Zárd le a függőségeidet. Ellenőrizd a hash-eket. Kontrollád a hálózati kimenő forgalmat. Ne bízz a postinstall scriptekben.
Ha még nem nézted meg a projektjeidet, csináld meg most. A kompromittált verziók az [email protected] és [email protected]. Ha bármelyik megjelenik a dependency fádban, tekintsd azt a gépet kompromittáltnak.