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:

  1. Végigauditáltam minden lockfile-t a kompromittált axios verziókra
  2. Minden npm install-t lecseréltem npm ci-re a CI-ban
  3. Hozzáadtam az ignore-scripts=true-t az .npmrc-hez minden projektben, explicit script futtatással a csomagoknak, amiknek kell
  4. Beépítettem az npm audit --audit-level=high-t blokkoló CI lépésként
  5. Telepítettem a friss-dependency ellenőrző scriptet
  6. Beállítottam egress policy-kat a GitHub Actions runnereinken
  7. 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.